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

Install the SDK: 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#

1

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.sh
bash
# 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"]
# }
2

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.

pkce.ts
typescript
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)
3

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.

token.ts
typescript
// 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.
4

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.

connect.ts
typescript
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')
5

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.

connectors.ts
typescript
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(', ')}`)
6

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.

metadata.ts
typescript
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`)
7

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.

annotations.ts
typescript
// 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'))    // false
8

Invoke 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.

invoke.ts
typescript
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.

mcpgate-integration.ts
typescript
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

If a connector has multiple accounts (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.