Integration Examples
This page walks through a complete TypeScript integration — from registering your OAuth client to invoking tools with full metadata-aware policy logic. The example uses the official MCP TypeScript SDK.
Prerequisites
npm install @modelcontextprotocol/sdk. You will need a registered OAuth client ID (MCPGATE_CLIENT_ID) and a valid access token (MCPGATE_ACCESS_TOKEN) obtained through the OAuth 2.0 + PKCE flow below.Step-by-step walkthrough#
Register your OAuth client
Call POST /oauth/register once to create a client registration. Store the returned client_id permanently — you reuse it for every authorization request. MCPGate uses public clients (no client secret); security comes from PKCE.
# Register your application once — store the returned client_id permanently
curl -s -X POST https://mcpgate.sh/oauth/register \
-H 'Content-Type: application/json' \
-d '{
"client_name": "My Agent App",
"redirect_uris": ["https://myapp.example.com/oauth/callback"]
}'
# Response:
# {
# "client_id": "mc_a1b2c3d4e5f6...",
# "client_name": "My Agent App",
# "redirect_uris": ["https://myapp.example.com/oauth/callback"]
# }Build the authorization URL with PKCE
Generate a PKCE code verifier and challenge, then redirect the user to the MCPGate authorization endpoint. MCPGate handles login via Clerk and presents an approval screen.
import crypto from 'crypto'
function generatePKCE() {
// Generate a cryptographically random code verifier (43–128 chars)
const codeVerifier = crypto.randomBytes(64).toString('base64url')
// Derive the code challenge: BASE64URL(SHA256(codeVerifier))
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url')
return { codeVerifier, codeChallenge }
}
function buildAuthorizeURL(clientId: string, codeChallenge: string, state: string): string {
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: 'https://myapp.example.com/oauth/callback',
response_type: 'code',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state,
})
return `https://mcpgate.sh/oauth/authorize?${params}`
}
// Usage:
const state = crypto.randomBytes(16).toString('hex') // CSRF protection
const { codeVerifier, codeChallenge } = generatePKCE()
const authorizeURL = buildAuthorizeURL(process.env.MCPGATE_CLIENT_ID!, codeChallenge, state)
// Store codeVerifier and state in session, then redirect the user:
// res.redirect(authorizeURL)Exchange the authorization code for a token
After the user approves, MCPGate redirects to your redirect_uri with an authorization code. Exchange it for a JWT access token using the code verifier.
// After the user approves and MCPGate redirects to your callback URL:
// GET /oauth/callback?code=AUTH_CODE&state=STATE
async function exchangeCodeForToken(
code: string,
codeVerifier: string
): Promise<{ access_token: string; token_type: string; expires_in: number }> {
const response = await fetch('https://mcpgate.sh/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.MCPGATE_CLIENT_ID!,
code,
code_verifier: codeVerifier,
redirect_uri: 'https://myapp.example.com/oauth/callback',
}),
})
if (!response.ok) {
const err = await response.text()
throw new Error(`Token exchange failed: ${err}`)
}
return response.json()
}
// The returned access_token is a JWT — store it securely.
// It is short-lived; implement refresh or re-authorization as needed.Connect to MCPGate
Create an SSE transport pointing at the MCPGate MCP endpoint and pass your OAuth access token as a bearer token. The endpoint is the same URL you paste into Claude Desktop or Cursor.
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
// MCPGATE_ACCESS_TOKEN is the JWT obtained from the token exchange above
const transport = new SSEClientTransport(
new URL('https://mcpgate.sh/mcp'),
{
requestInit: {
headers: {
Authorization: `Bearer ${process.env.MCPGATE_ACCESS_TOKEN}`,
},
},
}
)
const client = new Client({ name: 'my-agent', version: '1.0.0' }, { capabilities: {} })
await client.connect(transport)
console.log('Connected to MCPGate')Discover connected services
Call mcpgate_list_connectors to learn which services the user has connected. Use this to render an empty state if key services are missing.
const result = await client.callTool({
name: 'mcpgate_list_connectors',
arguments: {},
})
// Parse the JSON text content
const { connectors } = JSON.parse(result.content[0].text)
const connected = connectors.filter((c) => c.connected)
const disconnected = connectors.filter((c) => !c.connected)
console.log(`Connected services: ${connected.map((c) => c.name).join(', ')}`)
console.log(`Available but not connected: ${disconnected.map((c) => c.name).join(', ')}`)Fetch tool metadata
Call mcpgate_tool_metadata to get risk levels, categories, and action templates for every tool. Build a lookup map you can reference on each invocation.
const metaResult = await client.callTool({
name: 'mcpgate_tool_metadata',
arguments: {},
})
const { tools: toolMeta } = JSON.parse(metaResult.content[0].text)
// Build a lookup map: toolName → metadata
const metaByTool = Object.fromEntries(toolMeta.map((t) => [t.tool, t]))
// Example: separate tools by risk level
const readTools = toolMeta.filter((t) => t.riskLevel === 1)
const writeTools = toolMeta.filter((t) => t.riskLevel === 2)
const deleteTools = toolMeta.filter((t) => t.riskLevel === 3)
console.log(`Read tools (${readTools.length}): ${readTools.map((t) => t.title).join(', ')}`)
console.log(`Write tools (${writeTools.length})`)
console.log(`Delete tools (${deleteTools.length}) — require confirmation`)Read annotations from tools/list
The standard tools/list response includes MCP-spec annotations. Build a second lookup map so you can gate auto-approval and confirmation prompts.
// tools/list gives us annotations on each tool
const { tools } = await client.listTools()
// Build a lookup: toolName → annotations
const annotationsByTool = Object.fromEntries(
tools.map((t) => [t.name, t.annotations ?? {}])
)
function shouldAutoApprove(toolName: string): boolean {
const ann = annotationsByTool[toolName]
return ann?.readOnly === true
}
function requiresConfirmation(toolName: string): boolean {
const ann = annotationsByTool[toolName]
return ann?.destructive === true
}
// Usage:
console.log(shouldAutoApprove('gmail_read_email')) // true
console.log(requiresConfirmation('gmail_delete_email')) // true
console.log(shouldAutoApprove('gmail_send_email')) // falseInvoke tools with policy logic
Wrap tool invocations in a policy function that checks annotations before calling and logs activity entries using the action template afterwards.
async function invokeWithPolicy(
client: Client,
toolName: string,
args: Record<string, unknown>
): Promise<string> {
const ann = annotationsByTool[toolName] ?? {}
const meta = metaByTool[toolName]
// Destructive tools: require explicit user approval (implement your own prompt)
if (ann.destructive) {
const confirmed = await askUser(`Confirm: ${meta?.title ?? toolName}?`)
if (!confirmed) throw new Error('User cancelled')
}
const result = await client.callTool({ name: toolName, arguments: args })
const output = result.content[0].text
// Log to activity feed using action template
if (meta?.actionTemplate) {
const label = formatActivity(meta.actionTemplate, args)
logActivity({ tool: toolName, label, riskLevel: meta.riskLevel })
}
return output
}
function formatActivity(template: string, args: Record<string, unknown>): string {
return template.replace(/\{(\w+)\}/g, (_, key) =>
args[key] !== undefined ? String(args[key]) : `{${key}}`
)
}
// Example invocations:
const email = await invokeWithPolicy(client, 'gmail_read_email', {
messageId: '18d3f2a...',
})
// This will prompt the user before executing:
await invokeWithPolicy(client, 'gmail_delete_email', {
messageId: '18d3f2a...',
})Complete example#
The snippet below combines the connect-through-invoke steps into a single runnable file. It assumes you have already completed the OAuth flow and have a valid access token.
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
// Environment variables:
// MCPGATE_CLIENT_ID — from POST /oauth/register (permanent)
// MCPGATE_ACCESS_TOKEN — JWT from the token exchange (short-lived)
async function main() {
// 1. Connect using the OAuth access token
const transport = new SSEClientTransport(
new URL('https://mcpgate.sh/mcp'),
{ requestInit: { headers: { Authorization: `Bearer ${process.env.MCPGATE_ACCESS_TOKEN}` } } }
)
const client = new Client({ name: 'demo', version: '1.0.0' }, { capabilities: {} })
await client.connect(transport)
// 2. Discover connectors
const connResult = await client.callTool({ name: 'mcpgate_list_connectors', arguments: {} })
const { connectors } = JSON.parse(connResult.content[0].text)
console.log('Connected:', connectors.filter((c) => c.connected).map((c) => c.name))
// 3. Fetch metadata + annotations
const [metaResult, { tools }] = await Promise.all([
client.callTool({ name: 'mcpgate_tool_metadata', arguments: {} }),
client.listTools(),
])
const { tools: toolMeta } = JSON.parse(metaResult.content[0].text)
const metaByTool = Object.fromEntries(toolMeta.map((t) => [t.tool, t]))
const annotationsByTool = Object.fromEntries(tools.map((t) => [t.name, t.annotations ?? {}]))
// 4. Auto-approved read call
console.log('Annotations for gmail_read_email:', annotationsByTool['gmail_read_email'])
// → { title: 'Read Email', readOnly: true, idempotent: true, destructive: false, openWorldHint: true }
const readResult = await client.callTool({
name: 'gmail_read_email',
arguments: { messageId: '18d3f2a' },
})
console.log('Email:', readResult.content[0].text)
// 5. Activity feed entry
const meta = metaByTool['gmail_read_email']
const label = meta.actionTemplate.replace(/\{(\w+)\}/g, (_, k) => ({ messageId: '18d3f2a' })[k] ?? `{${k}}`)
console.log('Activity:', label) // → "Read email 18d3f2a"
await client.close()
}
main().catch(console.error)Multi-account tools
accountCount > 1 in the connector list), pass the desired account ID as the account parameter on any tool call targeting that connector. When omitted, MCPGate uses the account marked isDefault: true.What's next#
- Add Guardrail rules in the dashboard to enforce hard limits server-side, independent of your client policy logic.
- Check the Activity Log in the dashboard to verify that your tool calls are being recorded correctly.
- Review the Annotations and Metadata reference pages for the full field specifications.