Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 133 additions & 3 deletions extensions/simple-mcp-server/index.html.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
padding-bottom: 10px;
margin-top: 30px;
}
h3 {
color: #34495e;
margin-top: 20px;
margin-bottom: 10px;
}
p {
margin-bottom: 15px;
}
Expand Down Expand Up @@ -100,15 +105,77 @@
font-size: 0.9em;
color: #27ae60;
}
pre {
background-color: #f4f4f4;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-family: "Courier New", Courier, monospace;
font-size: 0.9em;
}
.info-box {
background-color: #e7f3ff;
border: 1px solid #b3d9ff;
border-left: 4px solid #2980b9;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.info-box strong {
color: #2980b9;
}
details {
background-color: #f9f9f9;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 10px;
}
details summary {
padding: 12px 15px;
cursor: pointer;
font-weight: 600;
color: #34495e;
list-style: none;
}
details summary::-webkit-details-marker {
display: none;
}
details summary::before {
content: "\25B6";
display: inline-block;
margin-right: 10px;
font-size: 0.8em;
transition: transform 0.2s;
}
details[open] summary::before {
transform: rotate(90deg);
}
details summary:hover {
background-color: #f0f0f0;
}
details .details-content {
padding: 0 15px 15px 15px;
}
details .details-content p {
margin-top: 0;
}
</style>
</head>
<body>
<div class="container">
<h1>{{ server_name }}</h1>

<p>This server provides sample data-related tools via the Model Context Protocol (MCP).</p>

<p>Please note that it is recommended to set the minimum number of instances/processes for
this application to >= 1 in the content settings. This will ensure that the MCP server is
<div class="info-box">
<strong>Visitor API Integration Required:</strong> The <code>connect_whoami</code> tool requires a
Visitor API Key integration to identify visitors. To enable this, go to the content settings in
Posit Connect and add a "Connect Visitor API Key" integration under the "Integrations" section.
See the <a href="https://docs.posit.co/connect/user/oauth-integrations/" target="_blank">OAuth Integrations documentation</a> for more information.
</div>

<p>Please note that it is recommended to set the minimum number of instances/processes for
this application to >= 1 in the content settings. This will ensure that the MCP server is
always available for clients to connect. See the <a href="https://docs.posit.co/connect/user/content-settings/index.html#process-configurations" target="_blank">content process configuration documentation</a>.</p>

<p>The MCP endpoint is available at:</p>
Expand All @@ -118,6 +185,69 @@
<span id="copyStatus"></span>
</div>

<h2>Setup Instructions</h2>

<details>
<summary>Claude Code</summary>
<div class="details-content">
<p>Run this command:</p>
<pre>claude mcp add --transport http simple-mcp {{ endpoint }} \
--header "Authorization: Key YOUR_POSIT_CONNECT_API_KEY"</pre>
</div>
</details>

<details>
<summary>Claude Desktop</summary>
<div class="details-content">
<p>Add to <code>~/Library/Application Support/Claude/claude_desktop_config.json</code> (macOS)
or <code>%APPDATA%\Claude\claude_desktop_config.json</code> (Windows):</p>
<pre>{
"mcpServers": {
"simple-mcp": {
"type": "streamable-http",
"url": "{{ endpoint }}",
"headers": {
"Authorization": "Key YOUR_POSIT_CONNECT_API_KEY"
}
}
}
}</pre>
</div>
</details>

<details>
<summary>VS Code</summary>
<div class="details-content">
<p>Add to <code>.vscode/mcp.json</code> in your workspace (or user settings):</p>
<pre>{
"servers": {
"simple-mcp": {
"type": "http",
"url": "{{ endpoint }}",
"headers": {
"Authorization": "Key YOUR_POSIT_CONNECT_API_KEY"
}
}
}
}</pre>
</div>
</details>

<details>
<summary>Cursor</summary>
<div class="details-content">
<p>Add to your MCP settings in Cursor preferences:</p>
<pre>{
"simple-mcp": {
"url": "{{ endpoint }}",
"headers": {
"Authorization": "Key YOUR_POSIT_CONNECT_API_KEY"
}
}
}</pre>
</div>
</details>

<h2>Available MCP Tools:</h2>
{% if tools %}
<ul>
Expand Down Expand Up @@ -147,7 +277,7 @@
function copyToClipboard() {
const endpointUrlField = document.getElementById('mcpEndpointUrl');
const copyStatus = document.getElementById('copyStatus');

navigator.clipboard.writeText(endpointUrlField.value).then(function() {
copyStatus.textContent = 'Copied!';
setTimeout(() => {
Expand Down
52 changes: 35 additions & 17 deletions extensions/simple-mcp-server/main.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
import contextlib
import json
import urllib

import pandas as pd
from sklearn.datasets import load_iris
from cachetools import TTLCache, cached
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastmcp import FastMCP, Context
from fastmcp.exceptions import ToolError
from posit.connect.client import Client as ConnectClient
import urllib
from posit import connect
from posit.connect.errors import ClientError
from sklearn.datasets import load_iris

# --- Connect Client Initialization ---
client = connect.Client()

# Create cache with TTL=1hour for visitor clients
client_cache = TTLCache(maxsize=float("inf"), ttl=3600)


@cached(client_cache)
def get_visitor_client(token: str | None) -> connect.Client:
"""Create and cache API client per token with 1 hour TTL"""
if token:
return client.with_user_session_token(token)
else:
return client

# --- FastMCP Server Initialization ---
mcp = FastMCP(
name="Simple MCP Server",
instructions="MCP server for dataset operations and Connect 'whoami' via FastAPI.",
stateless_http=True,
)

# --- Datasets ---
Expand Down Expand Up @@ -51,33 +70,32 @@ def calculate_summary_statistics(dataset_name: str) -> str:
@mcp.tool()
async def connect_whoami(context: Context) -> str:
"""
Calls the Posit Connect /me endpoint using an API key from the Authorization header.
The Authorization header should be in the format: 'Key YOUR_API_KEY'.
Calls the Posit Connect /me endpoint using the visitor's session token.
This tool requires a Visitor API Key integration to be configured.
"""

# context.request is a starlette.requests.Request
http_request = context.request_context.request
if http_request is None:
raise ToolError(
"Request context not available. This tool requires an HTTP-based transport."
)

auth_header = http_request.headers.get("x-mcp-authorization")
session_token = http_request.headers.get("posit-connect-user-session-token")

if not auth_header:
raise ToolError("Authorization header is missing.")

parts = auth_header.split()
if len(parts) != 2 or parts[0].lower() != "key":
if not session_token:
raise ToolError(
"Invalid Authorization header format. Expected 'Key YOUR_API_KEY'."
"Session token not available. This tool must be called from content running on Posit Connect."
)

api_key = parts[1]

try:
connect_client = ConnectClient(api_key=api_key)
return json.dumps(connect_client.me)
visitor_client = get_visitor_client(session_token)
return json.dumps(visitor_client.me)
except ClientError as e:
if e.error_code == 212:
raise ToolError(
"No Visitor API Key integration configured. Please add a Connect integration in the content settings."
)
raise ToolError(f"Error calling Connect API: {str(e)}")
except Exception as e:
raise ToolError(f"Error calling Connect API: {str(e)}")

Expand Down
5 changes: 3 additions & 2 deletions extensions/simple-mcp-server/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@
"category": "example",
"tags": ["python", "fastapi", "mcp"],
"requiredFeatures": [
"API Publishing"
"API Publishing",
"OAuth Integrations"
],
"minimumConnectVersion": "2025.04.0",
"version": "0.0.2"
"version": "0.0.3"
},
"files": {
"requirements.txt": {
Expand Down
1 change: 1 addition & 0 deletions extensions/simple-mcp-server/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
annotated-types==0.7.0
anyio==4.9.0
authlib==1.6.0
cachetools==5.5.2
certifi==2025.4.26
cffi==1.17.1 ; platform_python_implementation != 'PyPy'
charset-normalizer==3.4.2
Expand Down