Skip to content

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:

  1. Client registration (if auth is required)
  2. OAuth 2.1 authentication flow
  3. Making authenticated MCP requests
  4. 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:

javascript
// 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:

bash
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:

json
{
  "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:

javascript
// 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:

javascript
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:

  1. User is redirected to identity provider
  2. User logs in with their credentials
  3. User grants consent to your application
  4. Identity provider redirects back to your redirect_uri with authorization code

Callback URL:

http://localhost:8080/callback?code=AUTH_CODE&state=STATE_VALUE

Step 4: Exchange Code for Token

Exchange the authorization code for an access token:

javascript
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:

json
{
  "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 requests
  • refresh_token - Use to get new access tokens
  • expires_in - Token lifetime in seconds

Step 5: Refresh Tokens

When access token expires, use refresh token:

javascript
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 requests

Making MCP Requests

Initialize MCP Session

Start an MCP session:

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

CodeMeaningAction
-32001UnauthorizedRefresh or re-authenticate
-32002Invalid requestCheck request format
-32600Invalid JSON-RPCVerify JSON-RPC 2.0 format
-32601Method not foundCheck method name spelling
-32602Invalid paramsVerify parameter types/values
-32603Internal errorServer issue, contact admin

Client Libraries

JavaScript/TypeScript

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

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

  1. Store tokens securely - Use secure storage (keychain, encrypted storage)
  2. Handle expiration - Refresh tokens before they expire
  3. Implement retry logic - Auto-refresh on 401 errors
  4. Clean up - Clear tokens on logout

Connection Management

  1. Reuse connections - Keep HTTP connections alive
  2. Handle timeouts - Set reasonable timeout values
  3. Implement backoff - Use exponential backoff for retries
  4. Monitor health - Regularly check server availability

Error Handling

  1. Validate responses - Check for JSON-RPC errors
  2. Log failures - Record errors for debugging
  3. Provide feedback - Show meaningful errors to users
  4. Graceful degradation - Handle server unavailability

Testing

Mock Server

Create a mock MCP server for testing:

javascript
// 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

Getting Help

Released under the MIT License.