Building Custom MCP Clients
Integrate your own applications with the platform's MCP server
Overview
Any application that speaks MCP can connect to your platform. This guide covers the protocol details, authentication options, and implementation patterns for building custom MCP clients.
Protocol Basics
Your platform implements MCP spec 2025-03-26 over two transports:
HTTP Transport (Recommended)
- Endpoint:
POST /mcp/http - Body limit: 20 MB (supports base64 image uploads)
- CORS: Enabled (
Access-Control-Allow-Origin: *) - Session handling: Stateful via
Mcp-Session-Idheader, or stateless for single calls
Request flow:
POST /mcp/http
Headers:
X-API-Key: afk_YOUR_KEY
Content-Type: application/json
(optional) Mcp-Session-Id: SESSION_UUID
Body (JSON-RPC 2.0):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{ "name": "get_time", "description": "...", "inputSchema": {...} }
]
}
}
SSE Transport
- Endpoint:
GET /mcp/sse(opens stream) - Messages:
POST /mcp/messages?sessionId=XXX - Best for: Older MCP clients that don't support HTTP transport
SSE flow:
- Client opens
GET /mcp/ssewith auth headers - Server responds with SSE stream and sends
endpointevent - Client POSTs JSON-RPC to the endpoint URL
- Server responds via SSE
messageevents
Authentication Methods
Method 1: API Key (Simplest)
Add the X-API-Key header with a client-scoped key:
curl -X POST https://api.v2.affinitydesign.ca/mcp/http \
-H "X-API-Key: afk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"my-app","version":"1.0"}}}'
Pros: Simple, no token management, works everywhere
Cons: Key must be stored securely; no automatic expiry
Method 2: OAuth 2.1 (For User-Facing Apps)
If you're building an app that users log into:
-
Register your client:
POST /oauth/register { "client_name": "My Custom Client", "redirect_uris": ["https://myapp.com/callback"], "token_endpoint_auth_method": "none" } -
Initiate authorization:
GET /oauth/authorize?response_type=code&client_id=oauth_xxx& redirect_uri=https://myapp.com/callback& code_challenge=BASE64URL(SHA256(verifier))& code_challenge_method=S256& state=random_state -
User authenticates: They paste their
afk_...API key in the form -
Exchange code for tokens:
POST /oauth/token { "grant_type": "authorization_code", "code": "auth_code_from_callback", "redirect_uri": "https://myapp.com/callback", "code_verifier": "original_pkce_verifier", "client_id": "oauth_xxx" } -
Use the access token:
curl -X POST /mcp/http \ -H "Authorization: Bearer ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{...}'
Method 3: Admin JWT (Internal Tools)
For internal dashboards or admin scripts:
curl -X POST "https://api.v2.affinitydesign.ca/mcp/http?clientId=CLIENT_ID" \
-H "Authorization: Bearer ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Pass ?clientId=... to scope operations to a specific client.
JSON-RPC Methods
Your platform supports these methods:
| Method | Description |
|---|---|
initialize | Capability handshake. Must be first call on a session. |
notifications/initialized | Client ack (no response) |
tools/list | List all available tools |
tools/call | Execute a tool |
resources/list | List browsable resources |
resources/read | Read a resource by URI |
ping | Keep-alive probe |
Initialize Example
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"clientInfo": {
"name": "my-custom-client",
"version": "1.0.0"
},
"capabilities": {
"tools": {},
"resources": {}
}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": { "listChanged": false },
"resources": { "listChanged": false, "subscribe": false }
},
"serverInfo": {
"name": "af-gemini-connect",
"version": "1.0.0"
}
}
}
Tool Call Example
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_client_info",
"arguments": {}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "{\"clientId\":\"acme\",\"businessName\":\"Acme Corp\",...}"
}
],
"isError": false
}
}
Client Libraries
You don't need a library — MCP is just JSON-RPC over HTTP. But if you prefer:
- TypeScript:
@modelcontextprotocol/sdk— official SDK - Python:
mcppackage on PyPI - Go:
github.com/mark3labs/mcp-go
Asset Uploads
For large binary data (images, videos), don't base64-encode in tool arguments. Instead:
Option A: Direct Upload Endpoint
POST /mcp/assets/direct-upload
Headers:
X-API-Key: afk_YOUR_KEY
Content-Type: application/json
Body:
{
"filename": "hero-image.png",
"base64": "iVBORw0KGgo...",
"mimeType": "image/png",
"altText": "Homepage hero"
}
Response:
{
"data": {
"assetRef": "asset_abc123",
"canonicalFilename": "hero-image.png",
"mimeType": "image/png",
"sizeBytes": 45231
}
}
Then reference asset_abc123 in tool calls like wordpress_upload_image or github_upload_image.
Option B: Uploader Link
Call get_uploader_link to get a magic URL that opens the asset uploader pre-authenticated. Give this to users; they drop a file and get an assetRef back.
Error Handling
| HTTP Status | Meaning |
|---|---|
| 200 | Success (HTTP transport) |
| 202 | Accepted, no body (notifications) |
| 204 | Session terminated (DELETE) |
| 400 | Invalid JSON-RPC request |
| 401 | Missing or invalid auth |
| 404 | Session not found (SSE) |
| 500 | Tool execution error (check result.content for details) |
JSON-RPC errors use standard codes:
| Code | Meaning |
|---|---|
| -32600 | Invalid request |
| -32601 | Method not found |
| -32602 | Invalid params |
| -32700 | Parse error |
Session Management
HTTP sessions are evicted after 2 hours of inactivity. You can:
- Reuse the same
Mcp-Session-Idacross requests - Skip
initializefor stateless calls (the server creates a transient session) - Explicitly terminate with
DELETE /mcp/http+Mcp-Session-Idheader
Testing Your Client
Use these curl commands to verify connectivity:
# Initialize
export KEY="afk_YOUR_KEY"
export URL="https://api.v2.affinitydesign.ca/mcp/http"
INIT=$(curl -s -X POST "$URL" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test","version":"1.0"}}}')
SESSION=$(echo "$INIT" | grep -o '"Mcp-Session-Id": "[^"]*"' | cut -d'"' -f4)
# List tools
curl -s -X POST "$URL" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: $SESSION" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | jq '.result.tools | map(.name)'
# Get client info
curl -s -X POST "$URL" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: $SESSION" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_client_info","arguments":{}}}' | jq '.result.content[0].text | fromjson'
Security Checklist
Before deploying a custom client:
- Store API keys in environment variables, never in source code
- Use HTTPS only (enforced by server for OAuth redirects)
- Validate the origin in OAuth flows
- Handle token expiry gracefully (refresh or re-auth)
- Log MCP operations for audit purposes
- Restrict plugin access via key-level permissions
- Rotate keys regularly (every 90 days)
