Docs

Docs

MCP SDK Reference

sdk.mcp is the interface for registering your app's REST endpoints as MCP tools and invoking them through cpod-backend's tenant-scoped proxy.

Requires CPOD_APP_ID (or pass appId to the client constructor). All tool operations are scoped to this app.


Initialization#

typescript
import { CpodClient } from '@cpod/sdk'
 
// All three env vars required:
// CPOD_API_KEY    — Bearer token (from getAppToken() or manual exchange)
// CPOD_APP_ID     — app_id from POST /api/v1/apps/register
// CPOD_BASE_URL   — optional, defaults to https://api.cyberpod.app
const client = CpodClient.fromEnv()
 
// sdk.mcp is ready — all calls scoped to CPOD_APP_ID

mcp.register(tools) — Register tools#

Register one or more REST endpoints as MCP tools. Tools are upserted — calling register again with the same name updates the definition. Safe to call on every startup.

typescript
import type { McpTool } from '@cpod/sdk'
 
const tools: McpTool[] = [
  {
    name: 'get_vulnerability',           // snake_case, stable — renaming removes old tool
    description: 'Fetch a vulnerability record by ID. Returns full CVE details and affected assets.',
    endpoint: '/v1/risk/vulnerabilities/{id}',  // path on YOUR API
    method: 'GET',
    inputSchema: {
      type: 'object',
      properties: {
        id: { type: 'string', description: 'Vulnerability ID (e.g. vuln-001)' },
      },
      required: ['id'],
    },
  },
  {
    name: 'create_risk_item',
    description: 'Create a new risk item in the risk register.',
    endpoint: '/v1/risk/items',
    method: 'POST',
    inputSchema: {
      type: 'object',
      properties: {
        title:    { type: 'string', description: 'Short title' },
        severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
        owner_id: { type: 'string', description: 'Person ID of risk owner (per-...)' },
      },
      required: ['title', 'severity'],
    },
  },
]
 
const registered = await client.mcp.register(tools)
// registered: RegisteredMcpTool[] — includes id, appId, tenantId, createdAt

McpTool fields:

FieldTypeRequiredNotes
namestringyesSnake_case. Stable — renaming removes the old tool from the registry.
descriptionstringyesUsed by LLMs to decide when to call this tool. Be specific — include what IDs it expects and what it returns.
endpointstringyesREST path on your API. Path params use {param} syntax — extracted from arguments at call time.
methodstringyesGET, POST, PUT, PATCH, or DELETE.
inputSchemaobjectyesJSON Schema {type: "object", properties: {...}, required: [...]}.
maskResponsebooleannoWhen true, response text is run through PII masking before reaching the agent. Default false.
secretHeadersobjectno{"Header-Name": "vault-bundle-name"} — resolved at call time from vault, never stored in tool definition.
tagsstring[]noOptional labels for grouping tools in listings.

Write description as if explaining to a non-technical colleague. Bad: "Get vuln". Good: "Fetch a single vulnerability by its ID (e.g. 'vuln-001'). Returns CVE ID, severity, CVSS score, and list of affected asset IDs."


mcp.call(name, args) — Invoke a tool#

Call a registered tool through the proxy. The backend resolves path params, injects secret headers, applies PII masking if configured, and emits an audit event.

typescript
const result = await client.mcp.call('get_vulnerability', { id: 'vuln-001' })
console.log(result.content[0].text)
// → JSON string: {"id": "vuln-001", "severity": "high", "cvss": 8.1, ...}
 
// POST tool — body args
const created = await client.mcp.call('create_risk_item', {
  title: 'Exposed SSH port on prod-db-1',
  severity: 'high',
  owner_id: 'per-abc123',
})
console.log(created.content[0].text)

Calling another app's tools: if you're an agent calling tools registered by a different app, pass that app's app_id as the appId field in the request body — not your own. The SDK does this automatically when you construct the client with appId: "tool-owner-app-id". If calling the HTTP API directly: {"tool": "...", "appId": "tool-owner-app-id", "arguments": {...}}.

Your Bearer token still determines which tenant is searched — both you and the tool owner must be in the same tenant.

Path param extraction: arguments that match a {placeholder} in the endpoint path are substituted into the URL. Remaining arguments become query params (GET) or request body (POST/PUT/PATCH).

code
endpoint: /v1/risk/vulnerabilities/{id}
args:     { id: "vuln-001", format: "short" }

→ GET /v1/risk/vulnerabilities/vuln-001?format=short

mcp.list() — List registered tools#

typescript
const tools = await client.mcp.list()
for (const t of tools) {
  console.log(`${t.name}: ${t.endpoint} [${t.method}]`)
  console.log(`  created: ${t.createdAt}`)
}

mcp.unregister(name) — Remove a tool#

typescript
await client.mcp.unregister('get_vulnerability')

mcp.proxyInfo() — Get proxy URL#

Returns the URL agents should call, plus all registered tools.

typescript
const info = await client.mcp.proxyInfo()
console.log(info.proxyUrl)  // → https://api.cyberpod.app/api/v1/mcp/call
console.log(info.tools)     // RegisteredMcpTool[]

Give this URL to your agent config:

json
{
  "mcpServers": {
    "my-risk-app": {
      "url": "https://api.cyberpod.app/api/v1/mcp/call",
      "headers": { "Authorization": "Bearer <agent-token>" }
    }
  }
}

mcp.toMcpServer(tools) — Full MCP server#

Wrap your tools as an MCP-compatible server object. Use this to plug directly into the MCP TypeScript SDK, FastMCP, or any MCP transport — the calls still proxy through cpod-backend.

typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
 
const cpodServer = client.mcp.toMcpServer(tools, {
  name: 'my-risk-app',
  version: '1.0.0',
})
// cpodServer.tools()        → McpToolDefinition[]
// cpodServer.call(n, args)  → proxied through cpod-backend
 
const server = new McpServer({ name: cpodServer.name, version: cpodServer.version })
 
for (const tool of cpodServer.tools()) {
  server.tool(
    tool.name,
    tool.description,
    tool.inputSchema,
    async (args) => cpodServer.call(tool.name, args),
  )
}
 
await server.connect(new StdioServerTransport())
// or: StreamableHTTPServerTransport for HTTP/SSE

AI-First Controls#

These fields on McpTool activate per-call pipeline stages in cpod-backend. Set them when registering — no code changes needed in your API.

PII Masking (maskResponse)#

typescript
{
  name: 'get_employee',
  description: 'Fetch an employee profile. Email, phone, and SSN are masked in the response.',
  endpoint: '/v1/people/{id}',
  method: 'GET',
  inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
  maskResponse: true,   // ← response is scrubbed before reaching the agent
}

The masking engine strips emails, phone numbers, SSNs, credit card numbers, and other PII patterns, replacing them with masked tokens. Masking is fail-open — if the masking service is unavailable, the unmasked response is returned rather than failing the call.

Secret Header Injection (secretHeaders)#

typescript
{
  name: 'query_internal_api',
  description: 'Call the internal analytics API.',
  endpoint: '/v1/analytics/query',
  method: 'POST',
  inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
  secretHeaders: {
    'X-Analytics-Key': 'analytics-api-key',   // header_name → vault_bundle_name
    'X-Service-Token': 'internal-svc-token',
  },
}

At call time, cpod-backend resolves each vault bundle using the caller's claims and injects the values as headers to your upstream. The bundle names (analytics-api-key, internal-svc-token) must be registered in the secrets manager. Your upstream never needs to handle its own credential distribution. Resolution is fail-open — missing bundles are silently omitted.


Full Startup Pattern#

This is the recommended pattern for any service that exposes MCP tools:

typescript
import { getAppToken, CpodClient, type McpTool } from '@cpod/sdk'
 
// 1. Get token from service account credentials
const token = await getAppToken()
 
// 2. Build client
const client = new CpodClient({
  apiKey: token,
  appId: process.env.CPOD_APP_ID!,
})
 
// 3. Declare your tools
const tools: McpTool[] = [
  {
    name: 'list_vulnerabilities',
    description: 'List open vulnerabilities. Filter by severity or affected asset.',
    endpoint: '/v1/risk/vulnerabilities',
    method: 'GET',
    inputSchema: {
      type: 'object',
      properties: {
        severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
        asset_id: { type: 'string', description: 'Filter to vulnerabilities affecting this asset' },
        limit:    { type: 'integer', description: 'Max results (default 50)' },
      },
    },
  },
  {
    name: 'get_vulnerability',
    description: 'Fetch a single vulnerability by ID (vuln-...). Returns CVE, CVSS, affected assets.',
    endpoint: '/v1/risk/vulnerabilities/{id}',
    method: 'GET',
    inputSchema: {
      type: 'object',
      properties: {
        id: { type: 'string', description: 'Vulnerability ID, e.g. vuln-001' },
      },
      required: ['id'],
    },
  },
  {
    name: 'get_person',
    description: 'Fetch an employee profile. PII fields (email, phone) are automatically masked.',
    endpoint: '/v1/people/{id}',
    method: 'GET',
    inputSchema: {
      type: 'object',
      properties: {
        id: { type: 'string', description: 'Person ID, e.g. per-001' },
      },
      required: ['id'],
    },
    maskResponse: true,
  },
]
 
// 4. Register on startup (upsert — safe to call every boot)
await client.mcp.register(tools)
console.log(`Registered ${tools.length} MCP tools for app ${process.env.CPOD_APP_ID}`)
 
// 5. Expose as a full MCP server (optional — only if you need MCP transport)
const server = client.mcp.toMcpServer(tools, { name: 'risk-service', version: '1.0.0' })

Local Dev with the Emulator#

The emulator accepts any Bearer token — no OAuth setup needed:

bash
# Start emulator
cd emulator && node src/index.js
 
# Set env
export CPOD_API_URL=http://localhost:4000
export CPOD_API_KEY=dev-token
export CPOD_APP_ID=app_local_001  # from POST /v1/apps/register

Register your app first if you haven't:

bash
curl -X POST http://localhost:4000/v1/apps/register \
  -H "Authorization: Bearer dev-token" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-app"}' \
  | jq .app_id

Then your SDK code works unchanged — CpodClient.fromEnv() reads the env vars and all client.mcp.* calls hit the emulator.


Error Reference#

ErrorCauseFix
422 Unprocessable EntityTool missing name, endpoint, or methodAll three fields are required on every tool
404 Tool not registered on call()Tool name typo, or register() not calledCheck await client.mcp.list() to see what's registered
429 Too Many Requests120 req/min per caller exceededBack off and retry; consider caching tool call results
502 Bad Gatewaycpod-backend reached your API but it returned an errorCheck your API is running; check the upstream error in the 502 detail
CPOD_APP_ID not setMissing env varSet CPOD_APP_ID or pass appId to the client constructor

Pre-flight Checklist#

Before your first client.mcp.register() call:

  • CPOD_APP_ID is set — obtained from POST /api/v1/apps/register
  • CPOD_API_KEY is a valid Bearer token — use getAppToken() or set manually from token exchange
  • CPOD_API_URL points to the right environment (http://localhost:4000 or https://api.cyberpod.app)
  • client.mcp.list() runs without error (confirms auth + app_id are correct)

After registering tools:

  • client.mcp.list() shows your tools with correct name, endpoint, method
  • client.mcp.call("tool_name", {...}) returns a non-empty content[0].text
  • client.mcp.proxyInfo() returns the proxy URL your agents should use

For tools with AI controls:

  • maskResponse: true tools return masked output (emails/phones replaced with tokens)
  • secretHeaders tools — vault bundles are registered and accessible to your tenant

See Also#

  • Apps & MCP — registration, credentials, AI controls, security model
  • Agent Authentication — how external agents get tokens to call your tools
  • AuthenticationgetAppToken(), token cache pattern
  • MCP ProxytoMcpServer() transport wiring (stdio, HTTP/SSE, FastMCP)
  • Emulator — local dev without cloud credentials