Build on Captain
The Capture MCP SDK is currently only available to Capture's internal developers. We are working hard on the cybersecurity part, so that we can also offer it to everyone who builds on top of Capture.
The Capture MCP SDK is a library for the Capture MCP tool-registration protocol, for Python (capture-mcp) and for .NET (Capture.Cloud.Mcp.Sdk). Both behave the same, with the same names for settings and the same rules. A service that wants to expose tools to the Capture MCP server, and with that to Captain and to the AI tools of your users, uses the SDK to:
- Declare tools, with a decorator in Python and with the standard
[McpServerTool]attribute in .NET. - Register them at startup, and receive the calls to them.
You write ordinary functions. The SDK derives the tool schema from their signature, registers them, keeps the registration alive and runs your function when an agent calls it. You never write gRPC.
Beyond tools, the SDK also lets your app:
- tell Captain how to use your tools, with agents and skills
- gate each tool on a permission, see Tools
- know who is calling, see Authentication
- turn text into vectors with Capture's EU-hosted models, see Embeddings
- test all of this without a Capture environment, see Testing
Every code example in this section has a tab for Python and a tab for .NET. Your choice is remembered on all pages.
How it works
Registration and execution travel in opposite directions:
Your service ── 1. register tools, then a heartbeat ──▶ MCP server
Your service ◀── 2. ExecuteTool, when an agent calls ── MCP server
- Register. When your service starts, the SDK sends your tools to the MCP server. It repeats this as a heartbeat, every third of the time-to-live (TTL).
- Execute. When an agent calls one of your tools, the MCP server calls back into your service over gRPC. The SDK checks who is calling, runs your function and returns the result.
Two consequences follow:
- Your service needs a callback address the MCP server can reach. This is the most common thing to get wrong when you start. The SDK only checks that the address is a valid URI. A wrong DNS name or a blocking network policy fails silently until the first call to a tool, so if registration succeeds but a tool is never called, check the connection from the MCP server to your service first.
- A registration is a lease. If your service stops, or stops sending its heartbeat, its tools disappear by themselves once the lease runs out. There is nothing to clean up. A graceful shutdown unregisters them immediately.
If the MCP server is not available when your service starts, the service still starts. Registration keeps retrying in the background, with a backoff from 1 to 30 seconds, until it succeeds.
Install
- Python
- .NET
The SDK needs Python 3.12 or newer.
pip install capture-mcp
or, with uv:
uv add capture-mcp
Its dependencies (grpcio, httpx, mcp, protobuf and pydantic) are installed with it.
The SDK targets .NET 8 and builds on the standard ModelContextProtocol package, so tools use the [McpServerToolType] and [McpServerTool] attributes you already know. Add it to your service from the Capture NuGet feed:
dotnet add package Capture.Cloud.Mcp.Sdk
Tests use a separate package. It contains a fake token endpoint and a fake /api/me, which must never be reachable from a production dependency:
dotnet add package Capture.Cloud.Mcp.Sdk.Testing
Quick start
- Python
- .NET
import asyncio
import os
from capture_mcp_sdk import CaptureMcpApp, CaptureMcpCallContext
from capture_mcp_sdk.types import CaptureMcpSettings
settings = CaptureMcpSettings(
gateway_address="grpc://mcp-gateway:6001",
callback_address="grpc://my-service:50051",
service_name="my-service",
api_base_url="https://api.example.com/",
app_client_id="my-service-client",
app_client_secret=os.environ["MY_SERVICE_CLIENT_SECRET"],
)
app = CaptureMcpApp(settings)
@app.tool(read_only=True)
async def get_report(report_id: str) -> dict:
"""Fetch a report by ID."""
return {"id": report_id, "status": "ok"}
@app.tool(requires_permission="events:view")
async def get_logbook_events() -> dict:
"""Returns logbook events for one Capture company, newest first."""
return {"events": []}
@app.tool(data_intensive=True)
async def export_data(query: str, limit: int = 100) -> dict:
"""Export data matching the query."""
return {"rows": []}
async def main() -> None:
await app.start()
# keep alive until shutdown signal
await asyncio.Event().wait()
asyncio.run(main())
app.start() returns right away. Registration runs in the background, so your service does not wait for the MCP server. On a graceful shutdown, call await app.stop() to unregister your tools straight away.
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddCaptureMcpTools(builder.Configuration.GetSection("Mcp").Bind)
.WithToolsFromAssembly();
var app = builder.Build();
app.MapCaptureMcpTools();
app.Run();
{
"Mcp": {
"GatewayAddress": "http://mcp-gateway:6001",
"CallbackAddress": "http://my-service:50051",
"ServiceName": "my-service",
"ApiBaseUrl": "https://api.example.com/",
"AppClientId": "my-service-client"
}
}
Supply the secret from the environment, as Mcp__AppClientSecret, never from a file in source control.
using System.ComponentModel;
using ModelContextProtocol.Server;
public sealed record ReportStatus(string Id, string Status);
public sealed record LogbookEvents(IReadOnlyList<string> Events);
public sealed record ExportResult(IReadOnlyList<string> Rows);
[McpServerToolType]
public sealed class ReportTools
{
[McpServerTool(ReadOnly = true), Description("Fetch a report by ID.")]
public static ReportStatus GetReport(string reportId) => new(reportId, "ok");
[McpServerTool, RequiresPermission("events:view")]
[Description("Returns logbook events for one Capture company, newest first.")]
public static LogbookEvents GetLogbookEvents() => new([]);
[McpServerTool, DataIntensive, Description("Export data matching the query.")]
public static ExportResult ExportData(string query, int limit = 100) => new([]);
}
On startup, a hosted service registers the tools with the MCP server, and the MCP server calls back into your service over gRPC. MapCaptureMcpTools() hosts that callback, so the port in CallbackAddress has to serve HTTP/2.
The addresses in these examples are placeholders. Ask your Capture contact for the real ones for your environment.
Settings
The settings are the same in both SDKs. In Python they are fields of CaptureMcpSettings, in .NET they are the options of CaptureMcpOptions, bound from the configuration section you pass to AddCaptureMcpTools. Addresses must be absolute URIs, and a wrong value fails when the settings are created, not at the first call.
| Python | .NET | Required | Description |
|---|---|---|---|
gateway_address | GatewayAddress | Yes | The address of the MCP server that your tools register with. |
callback_address | CallbackAddress | Yes | The address at which the MCP server can reach your service to execute a tool. It must be reachable from the MCP server. |
service_name | ServiceName | Yes | The name of the group your tools belong to. Pick a name of your own, not the name of a built-in group, and not a name shaped like app-123. Both are refused. |
platform_app_name | PlatformAppName | No | When set, tool names are prefixed with this name instead of the service name. |
api_base_url | ApiBaseUrl | Yes | The root of the Capture API, not one endpoint under it. |
app_client_id, app_client_secret | AppClientId, AppClientSecret | Yes | The credentials of your app. See where they come from. |
capture_ai_base_url | CaptureAiBaseUrl | No | The address of Captain, including any path prefix. Only the embeddings client needs it. |
ttl_seconds | TtlSeconds | No | How long a registration lasts without a heartbeat. The default is 90, so the heartbeat runs every 30 seconds. |
me_cache_ttl_seconds | MeCacheTtlSeconds | No | How long the SDK caches who a caller is. The default is 60. See caller identity caching. |
agents_dir, skills_dir | CaptureMcpAgentsDir, CaptureMcpSkillsDir | No | Where your agents and skills live. In .NET these are MSBuild properties. |
Where the app credentials come from
api_base_url is the root of the Capture API. The SDK derives every route it needs from it: the token endpoint for your registration credential, api/me to check a caller, and the routes to find your app and declare its permissions. Point it at the Capture API, not at the legacy authentication service: only the Capture API has a /connect/token endpoint, and a token from the legacy service cannot register tools.
Always write the base address as a root, such as https://api.example.com/. The .NET SDK resolves every route against it, which drops the last segment of an address that has a path but no trailing slash. A base like that logs a warning at startup. The Python SDK accepts it either way.
app_client_id and app_client_secret are the confidential client of your own custom app, the same OpenID Connect client_id and secret you use to declare the roles and permissions of your app. They are not separate credentials for calling the API, and they are not the token of the person calling your tool. The SDK only uses them where your app acts as itself: registering tools, finding its app key and declaring its permission catalog.
Ask a Capture administrator for them if you do not have them yet. The client has to be linked to your custom app, otherwise the SDK cannot find your app key and skips declaring permissions.
Read the secret from the environment or a secret store, never from a literal in your code. In Python the setting is a SecretStr, so it is masked in repr() and in logs.
Version 0.2.0 renamed some settings, and this is a breaking change. api_token_endpoint is gone: pass api_base_url instead, and the SDK derives the token endpoint from it. api_client_id and api_client_secret are now app_client_id and app_client_secret. me_cache_ttl_seconds was added. The names now match the .NET SDK field for field.
Tool names
An agent calls a tool by the name it is registered under, {prefix}_{tool}. The prefix is platform_app_name (PlatformAppName in .NET) when you set it, and the service name otherwise. The SDK lower-cases the name and replaces every character outside a-z, 0-9 and _ with _. The result is at most 128 characters.
With the service name my-service, the Python function get_report and the .NET method GetReport are both registered as my_service_get_report. With platform_app_name="uns", a tool browse becomes uns_browse.
Keep tool names unique within a service. In Python, two functions that resolve to the same registered name are refused when you declare them.
What is in the package
- Python
- .NET
Everything you need is available from capture_mcp_sdk. Names are loaded on first use, so importing the package for an identity does not load grpc or the generated stubs.
| Import | What it is for |
|---|---|
capture_mcp_sdk | CaptureMcpApp, CaptureMcpCallContext, add_capture_mcp_tools, capture_app_identity and the embeddings client. |
capture_mcp_sdk.types | CaptureMcpSettings and CaptureAppSettings. |
capture_mcp_sdk.auth | The identity building blocks, without any gRPC. See Authentication. |
capture_mcp_sdk.testing | In-process doubles for the MCP server and the embeddings endpoint. Tests only. See Testing. |
capture-mcp (command line) | capture-mcp validate, which checks your agents and skills. See Agents and skills. |
| Package | What it is for |
|---|---|
Capture.Cloud.Mcp.Sdk | AddCaptureMcpTools and MapCaptureMcpTools, [RequiresPermission], [DataIntensive], ICaptureMcpCallContext, AddCaptureAppIdentity with ICaptureAppIdentity, AddCaptureEmbeddings with ICaptureEmbeddingsClient, and a Roslyn analyzer that checks your agents and skills during the build. |
Capture.Cloud.Mcp.Sdk.Testing | CaptureMcpMockServer, an in-process double for the MCP server, the Capture API and the embeddings endpoint. Tests only. See Testing. |