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#
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_IDfrom cpod import CpodClient
# CPOD_API_KEY, CPOD_APP_ID (and optionally CPOD_API_URL) from env
client = CpodClient.from_env()
# client.mcp is readyimport cpod "github.com/cpod-ai/cpod-sdk-go"
// CPOD_API_KEY, CPOD_APP_ID (and optionally CPOD_BASE_URL) from env
client, err := cpod.NewFromEnv()
// client.MCP is readyusing Cpod.SDK;
// CPOD_API_KEY, CPOD_APP_ID (and optionally CPOD_BASE_URL) from env
var client = CpodClient.FromEnv();
// client.Mcp is readymcp.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.
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, createdAtfrom cpod.mcp.types import McpTool, McpToolInputSchema
tools = [
McpTool(
name="get_vulnerability",
description="Fetch a vulnerability record by ID. Returns full CVE details and affected assets.",
endpoint="/v1/risk/vulnerabilities/{id}",
method="GET",
input_schema=McpToolInputSchema(
properties={
"id": {"type": "string", "description": "Vulnerability ID (e.g. vuln-001)"},
},
required=["id"],
),
),
McpTool(
name="create_risk_item",
description="Create a new risk item in the risk register.",
endpoint="/v1/risk/items",
method="POST",
input_schema=McpToolInputSchema(
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"},
},
required=["title", "severity"],
),
),
]
registered = await client.mcp.register(tools)
# registered: list[RegisteredMcpTool]import "github.com/cpod-ai/cpod-sdk-go/mcp"
tools := []mcp.McpTool{
{
Name: "get_vulnerability",
Description: "Fetch a vulnerability record by ID. Returns full CVE details and affected assets.",
Endpoint: "/v1/risk/vulnerabilities/{id}",
Method: "GET",
InputSchema: mcp.McpToolInputSchema{
Type: "object",
Properties: map[string]map[string]interface{}{
"id": {"type": "string", "description": "Vulnerability ID (e.g. vuln-001)"},
},
Required: []string{"id"},
},
},
{
Name: "create_risk_item",
Description: "Create a new risk item in the risk register.",
Endpoint: "/v1/risk/items",
Method: "POST",
InputSchema: mcp.McpToolInputSchema{
Type: "object",
Properties: map[string]map[string]interface{}{
"title": {"type": "string", "description": "Short title"},
"severity": {"type": "string", "enum": []string{"low", "medium", "high", "critical"}},
"owner_id": {"type": "string", "description": "Person ID of risk owner"},
},
Required: []string{"title", "severity"},
},
},
}
registered, err := client.MCP.Register(ctx, tools)using Cpod.SDK.Mcp;
var tools = new[]
{
new McpTool
{
Name = "get_vulnerability",
Description = "Fetch a vulnerability record by ID. Returns full CVE details.",
Endpoint = "/v1/risk/vulnerabilities/{id}",
Method = "GET",
InputSchema = new McpToolInputSchema
{
Properties = new Dictionary<string, Dictionary<string, object>>
{
["id"] = new() { ["type"] = "string", ["description"] = "Vulnerability ID" },
},
Required = ["id"],
},
},
new McpTool
{
Name = "create_risk_item",
Description = "Create a new risk item in the risk register.",
Endpoint = "/v1/risk/items",
Method = "POST",
InputSchema = new McpToolInputSchema
{
Properties = new Dictionary<string, Dictionary<string, object>>
{
["title"] = new() { ["type"] = "string" },
["severity"] = new() { ["type"] = "string",
["enum"] = new[] { "low", "medium", "high", "critical" } },
["owner_id"] = new() { ["type"] = "string" },
},
Required = ["title", "severity"],
},
},
};
var registered = await client.Mcp.RegisterAsync(tools);McpTool fields:
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Snake_case. Stable — renaming removes the old tool from the registry. |
description | string | yes | Used by LLMs to decide when to call this tool. Be specific — include what IDs it expects and what it returns. |
endpoint | string | yes | REST path on your API. Path params use {param} syntax — extracted from arguments at call time. |
method | string | yes | GET, POST, PUT, PATCH, or DELETE. |
inputSchema | object | yes | JSON Schema {type: "object", properties: {...}, required: [...]}. |
maskResponse | boolean | no | When true, response text is run through PII masking before reaching the agent. Default false. |
secretHeaders | object | no | {"Header-Name": "vault-bundle-name"} — resolved at call time from vault, never stored in tool definition. |
tags | string[] | no | Optional 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.
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)result = await client.mcp.call("get_vulnerability", {"id": "vuln-001"})
print(result.content[0]["text"])
# → JSON string: {"id": "vuln-001", "severity": "high", ...}
# POST tool — body args
created = await client.mcp.call("create_risk_item", {
"title": "Exposed SSH port on prod-db-1",
"severity": "high",
"owner_id": "per-abc123",
})
print(created.content[0]["text"])result, err := client.MCP.Call(ctx, "get_vulnerability", map[string]interface{}{
"id": "vuln-001",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Content[0].Text)
// POST tool
created, err := client.MCP.Call(ctx, "create_risk_item", map[string]interface{}{
"title": "Exposed SSH port on prod-db-1",
"severity": "high",
"owner_id": "per-abc123",
})var result = await client.Mcp.CallAsync("get_vulnerability",
new Dictionary<string, object> { ["id"] = "vuln-001" });
Console.WriteLine(result.Content[0].Text);
// POST tool
var created = await client.Mcp.CallAsync("create_risk_item",
new Dictionary<string, object>
{
["title"] = "Exposed SSH port on prod-db-1",
["severity"] = "high",
["owner_id"] = "per-abc123",
});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).
endpoint: /v1/risk/vulnerabilities/{id}
args: { id: "vuln-001", format: "short" }
→ GET /v1/risk/vulnerabilities/vuln-001?format=short
mcp.list() — List registered tools#
const tools = await client.mcp.list()
for (const t of tools) {
console.log(`${t.name}: ${t.endpoint} [${t.method}]`)
console.log(` created: ${t.createdAt}`)
}tools = await client.mcp.list()
for t in tools:
print(f"{t.name}: {t.endpoint} [{t.method}]")tools, err := client.MCP.List(ctx)
for _, t := range tools {
fmt.Printf("%s: %s [%s]\n", t.Name, t.Endpoint, t.Method)
}var tools = await client.Mcp.ListAsync();
foreach (var t in tools)
Console.WriteLine($"{t.Name}: {t.Endpoint} [{t.Method}]");mcp.unregister(name) — Remove a tool#
await client.mcp.unregister('get_vulnerability')await client.mcp.unregister("get_vulnerability")err := client.MCP.Unregister(ctx, "get_vulnerability")await client.Mcp.UnregisterAsync("get_vulnerability");mcp.proxyInfo() — Get proxy URL#
Returns the URL agents should call, plus all registered tools.
const info = await client.mcp.proxyInfo()
console.log(info.proxyUrl) // → https://api.cyberpod.app/api/v1/mcp/call
console.log(info.tools) // RegisteredMcpTool[]info = await client.mcp.proxy_info()
print(info.proxy_url) # → https://api.cyberpod.app/api/v1/mcp/call
print(info.tools) # list[RegisteredMcpTool]info, err := client.MCP.ProxyInfo(ctx)
fmt.Println(info.ProxyURL)var info = await client.Mcp.ProxyInfoAsync();
Console.WriteLine(info.ProxyUrl);Give this URL to your agent config:
{
"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.
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/SSEfrom fastmcp import FastMCP
# Build the cpod server wrapper
mcp_server = client.mcp.to_mcp_server(tools)
# mcp_server.list_tools() → list[McpToolDefinition]
# mcp_server.call_tool(n, args) → McpCallResult
app = FastMCP("my-risk-app")
for tool_def in mcp_server.list_tools():
def make_handler(name: str, description: str):
@app.tool(name=name, description=description)
async def handler(**kwargs):
result = await mcp_server.call_tool(name, kwargs)
return result.content[0]["text"]
return handler
make_handler(tool_def.name, tool_def.description)
# HTTP server on :3001
app.run(transport="streamable-http", host="0.0.0.0", port=3001)The make_handler wrapper is required to capture tool_def in the closure. Omitting it causes all handlers to call the last tool in the list.
cpodServer := client.MCP.ToMcpServer(tools)
// cpodServer.ListTools() → []McpToolDefinition
// cpodServer.CallTool(ctx, n, args) → *McpCallResult
// Wire into any MCP Go SDK handler, for example:
for _, def := range cpodServer.ListTools() {
name := def.Name // capture in closure
mcpHandler.RegisterTool(def, func(ctx context.Context, args map[string]interface{}) (string, error) {
result, err := cpodServer.CallTool(ctx, name, args)
if err != nil {
return "", err
}
return result.Content[0].Text, nil
})
}var cpodServer = client.Mcp.ToMcpServer(tools);
// cpodServer.ListTools() → IReadOnlyList<McpToolDefinition>
// cpodServer.CallToolAsync(name, args, ct) → McpCallResult
// Wire into any MCP .NET host:
foreach (var def in cpodServer.ListTools())
{
var toolName = def.Name; // capture
host.RegisterTool(def, async (args, ct) =>
{
var result = await cpodServer.CallToolAsync(toolName, args, ct);
return result.Content[0].Text;
});
}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)#
{
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)#
{
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:
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' })import asyncio
import os
from cpod.auth import get_app_token
from cpod import CpodClient
from cpod.mcp.types import McpTool, McpToolInputSchema
async def main():
# 1. Get token from service account credentials
token = await get_app_token()
# 2. Build client
client = CpodClient(
api_key=token,
app_id=os.environ["CPOD_APP_ID"],
)
# 3. Declare tools
tools = [
McpTool(
name="list_vulnerabilities",
description="List open vulnerabilities. Filter by severity or affected asset.",
endpoint="/v1/risk/vulnerabilities",
method="GET",
input_schema=McpToolInputSchema(
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)"},
},
),
),
McpTool(
name="get_vulnerability",
description="Fetch a single vulnerability by ID (vuln-...). Returns CVE, CVSS, affected assets.",
endpoint="/v1/risk/vulnerabilities/{id}",
method="GET",
input_schema=McpToolInputSchema(
properties={
"id": {"type": "string", "description": "Vulnerability ID, e.g. vuln-001"},
},
required=["id"],
),
),
McpTool(
name="get_person",
description="Fetch an employee profile. PII fields (email, phone) are automatically masked.",
endpoint="/v1/people/{id}",
method="GET",
input_schema=McpToolInputSchema(
properties={
"id": {"type": "string", "description": "Person ID, e.g. per-001"},
},
required=["id"],
),
mask_response=True,
),
]
# 4. Register on startup (upsert — safe to call every boot)
await client.mcp.register(tools)
print(f"Registered {len(tools)} MCP tools for app {os.environ['CPOD_APP_ID']}")
asyncio.run(main())Local Dev with the Emulator#
The emulator accepts any Bearer token — no OAuth setup needed:
# 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/registerRegister your app first if you haven't:
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_idThen your SDK code works unchanged — CpodClient.fromEnv() reads the env vars and all client.mcp.* calls hit the emulator.
Error Reference#
| Error | Cause | Fix |
|---|---|---|
422 Unprocessable Entity | Tool missing name, endpoint, or method | All three fields are required on every tool |
404 Tool not registered on call() | Tool name typo, or register() not called | Check await client.mcp.list() to see what's registered |
429 Too Many Requests | 120 req/min per caller exceeded | Back off and retry; consider caching tool call results |
502 Bad Gateway | cpod-backend reached your API but it returned an error | Check your API is running; check the upstream error in the 502 detail |
CPOD_APP_ID not set | Missing env var | Set CPOD_APP_ID or pass appId to the client constructor |
Pre-flight Checklist#
Before your first client.mcp.register() call:
-
CPOD_APP_IDis set — obtained fromPOST /api/v1/apps/register -
CPOD_API_KEYis a valid Bearer token — usegetAppToken()or set manually from token exchange -
CPOD_API_URLpoints to the right environment (http://localhost:4000orhttps://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 correctname,endpoint,method -
client.mcp.call("tool_name", {...})returns a non-emptycontent[0].text -
client.mcp.proxyInfo()returns the proxy URL your agents should use
For tools with AI controls:
-
maskResponse: truetools return masked output (emails/phones replaced with tokens) -
secretHeaderstools — 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
- Authentication —
getAppToken(), token cache pattern - MCP Proxy —
toMcpServer()transport wiring (stdio, HTTP/SSE, FastMCP) - Emulator — local dev without cloud credentials