Custom MCP Clients
Guide for integrating custom MCP clients with Seed, including client registration, authentication, and making MCP requests.
Overview
If you're building your own MCP client or integrating an existing one with Seed, this guide covers:
- Client registration (if auth is required)
- OAuth 2.1 authentication flow
- Making authenticated MCP requests
- Handling errors and edge cases
Prerequisites
- Understanding of MCP (Model Context Protocol)
- Familiarity with OAuth 2.0/2.1
- HTTP client library in your language
- Seed server URL and configuration details
Quick Start
To connect your custom client to Seed:
// 1. Register client (once)
// 2. Get authorization code (OAuth flow)
// 3. Exchange for access token
// 4. Make MCP requests with token
const response = await fetch('https://mcp.example.com/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/list',
id: 1
})
});Authentication Flow
Step 1: Dynamic Client Registration
Register your client with Seed (required for OAuth):
Request:
curl -X POST https://mcp.example.com/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Custom Client",
"redirect_uris": ["http://localhost:8080/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'Response:
{
"client_id": "seed-abc123xyz",
"client_name": "My Custom Client",
"redirect_uris": ["http://localhost:8080/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"client_id_issued_at": 1702080000,
"client_secret_expires_at": 0
}Save your client_id - you'll need it for the OAuth flow.
Step 2: Generate PKCE Challenge
Generate PKCE (Proof Key for Code Exchange) parameters:
// Generate code verifier (43-128 random characters)
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
// Generate code challenge (SHA-256 hash of verifier)
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(hash));
}
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);Important: Store codeVerifier securely - you'll need it in Step 4.
Step 3: Start Authorization Flow
Redirect user to authorization endpoint:
const authUrl = new URL('https://mcp.example.com/oauth/authorize');
authUrl.searchParams.append('client_id', 'seed-abc123xyz');
authUrl.searchParams.append('redirect_uri', 'http://localhost:8080/callback');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('scope', 'openid profile email');
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
authUrl.searchParams.append('state', generateRandomState());
// Redirect user to authUrl
window.location.href = authUrl.toString();User Flow:
- User is redirected to identity provider
- User logs in with their credentials
- User grants consent to your application
- Identity provider redirects back to your
redirect_uriwith authorization code
Callback URL:
http://localhost:8080/callback?code=AUTH_CODE&state=STATE_VALUEStep 4: Exchange Code for Token
Exchange the authorization code for an access token:
const tokenResponse = await fetch('https://mcp.example.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: 'seed-abc123xyz',
redirect_uri: 'http://localhost:8080/callback',
code: authorizationCode,
code_verifier: codeVerifier // From Step 2
})
});
const tokens = await tokenResponse.json();Response:
{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "refresh_token_value",
"scope": "openid profile email"
}Save these tokens:
access_token- Use for MCP requestsrefresh_token- Use to get new access tokensexpires_in- Token lifetime in seconds
Step 5: Refresh Tokens
When access token expires, use refresh token:
const refreshResponse = await fetch('https://mcp.example.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: 'seed-abc123xyz',
refresh_token: refreshToken
})
});
const newTokens = await refreshResponse.json();
// Use newTokens.access_token for subsequent requestsMaking MCP Requests
Initialize MCP Session
Start an MCP session:
const initResponse = await fetch('https://mcp.example.com/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {
tools: {},
prompts: {},
resources: {}
},
clientInfo: {
name: 'My Custom Client',
version: '1.0.0'
}
},
id: 1
})
});
const initResult = await initResponse.json();List Available Tools
const toolsResponse = await fetch('https://mcp.example.com/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/list',
id: 2
})
});
const tools = await toolsResponse.json();
console.log(tools.result.tools);Call a Tool
const callResponse = await fetch('https://mcp.example.com/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/call',
params: {
name: 'echo',
arguments: {
message: 'Hello from custom client'
}
},
id: 3
})
});
const result = await callResponse.json();
console.log(result.result.content[0].text);List Prompts
const promptsResponse = await fetch('https://mcp.example.com/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'prompts/list',
id: 4
})
});Get a Prompt
const getPromptResponse = await fetch('https://mcp.example.com/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'prompts/get',
params: {
name: 'greeting',
arguments: {
name: 'Alice',
style: 'formal'
}
},
id: 5
})
});List Resources
const resourcesResponse = await fetch('https://mcp.example.com/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'resources/list',
id: 6
})
});Read a Resource
const readResponse = await fetch('https://mcp.example.com/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'resources/read',
params: {
uri: 'config://server'
},
id: 7
})
});Error Handling
HTTP Errors
async function makeMCPRequest(method, params, accessToken) {
const response = await fetch('https://mcp.example.com/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({
jsonrpc: '2.0',
method,
params,
id: Date.now()
})
});
if (!response.ok) {
if (response.status === 401) {
// Token expired, refresh it
await refreshAccessToken();
return makeMCPRequest(method, params, newAccessToken);
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.error) {
throw new Error(`MCP Error ${data.error.code}: ${data.error.message}`);
}
return data.result;
}Common Error Codes
| Code | Meaning | Action |
|---|---|---|
| -32001 | Unauthorized | Refresh or re-authenticate |
| -32002 | Invalid request | Check request format |
| -32600 | Invalid JSON-RPC | Verify JSON-RPC 2.0 format |
| -32601 | Method not found | Check method name spelling |
| -32602 | Invalid params | Verify parameter types/values |
| -32603 | Internal error | Server issue, contact admin |
Client Libraries
JavaScript/TypeScript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { HTTPClientTransport } from '@modelcontextprotocol/sdk/client/http.js';
const client = new Client({
name: 'my-custom-client',
version: '1.0.0'
}, {
capabilities: {
tools: {},
prompts: {},
resources: {}
}
});
const transport = new HTTPClientTransport({
url: 'https://mcp.example.com/mcp',
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
await client.connect(transport);
// List tools
const tools = await client.listTools();
// Call tool
const result = await client.callTool({
name: 'echo',
arguments: { message: 'Hello' }
});Python
import httpx
import json
class MCPClient:
def __init__(self, base_url, access_token=None):
self.base_url = base_url
self.access_token = access_token
self.request_id = 1
def _make_request(self, method, params=None):
headers = {'Content-Type': 'application/json'}
if self.access_token:
headers['Authorization'] = f'Bearer {self.access_token}'
payload = {
'jsonrpc': '2.0',
'method': method,
'id': self.request_id
}
if params:
payload['params'] = params
self.request_id += 1
response = httpx.post(
f'{self.base_url}/mcp',
headers=headers,
json=payload
)
response.raise_for_status()
return response.json()
def list_tools(self):
return self._make_request('tools/list')
def call_tool(self, name, arguments):
return self._make_request('tools/call', {
'name': name,
'arguments': arguments
})
# Usage
client = MCPClient('https://mcp.example.com', access_token)
tools = client.list_tools()
result = client.call_tool('echo', {'message': 'Hello'})Best Practices
Token Management
- Store tokens securely - Use secure storage (keychain, encrypted storage)
- Handle expiration - Refresh tokens before they expire
- Implement retry logic - Auto-refresh on 401 errors
- Clean up - Clear tokens on logout
Connection Management
- Reuse connections - Keep HTTP connections alive
- Handle timeouts - Set reasonable timeout values
- Implement backoff - Use exponential backoff for retries
- Monitor health - Regularly check server availability
Error Handling
- Validate responses - Check for JSON-RPC errors
- Log failures - Record errors for debugging
- Provide feedback - Show meaningful errors to users
- Graceful degradation - Handle server unavailability
Testing
Mock Server
Create a mock MCP server for testing:
// Mock server for unit tests
const mockServer = {
'tools/list': { tools: [] },
'tools/call': { content: [{ type: 'text', text: 'mocked' }] }
};
async function mockFetch(url, options) {
const request = JSON.parse(options.body);
return {
ok: true,
json: async () => ({
jsonrpc: '2.0',
result: mockServer[request.method],
id: request.id
})
};
}Next Steps
- API Reference - Complete API documentation
- MCP Protocol Spec - Official MCP specification
- Tools Overview - Available tools
- Authentication Flow - Deep dive into auth
Getting Help
- OAuth issues: See OAuth 2.1 Implementation
- MCP protocol: Check MCP specification
- Server problems: Contact your Seed server administrator
- Integration help: See Developer Guide