Skip to main content

Build on Captain

Internal developers only

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:

  1. Declare tools, with a decorator in Python and with the standard [McpServerTool] attribute in .NET.
  2. 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
  1. 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).
  2. 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​

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.


Quick start​

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.

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.NETRequiredDescription
gateway_addressGatewayAddressYesThe address of the MCP server that your tools register with.
callback_addressCallbackAddressYesThe address at which the MCP server can reach your service to execute a tool. It must be reachable from the MCP server.
service_nameServiceNameYesThe 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_namePlatformAppNameNoWhen set, tool names are prefixed with this name instead of the service name.
api_base_urlApiBaseUrlYesThe root of the Capture API, not one endpoint under it.
app_client_id, app_client_secretAppClientId, AppClientSecretYesThe credentials of your app. See where they come from.
capture_ai_base_urlCaptureAiBaseUrlNoThe address of Captain, including any path prefix. Only the embeddings client needs it.
ttl_secondsTtlSecondsNoHow long a registration lasts without a heartbeat. The default is 90, so the heartbeat runs every 30 seconds.
me_cache_ttl_secondsMeCacheTtlSecondsNoHow long the SDK caches who a caller is. The default is 60. See caller identity caching.
agents_dir, skills_dirCaptureMcpAgentsDir, CaptureMcpSkillsDirNoWhere 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.

Give the root, with a trailing slash

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.

Upgrading the Python SDK to 0.2.0

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​

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.

ImportWhat it is for
capture_mcp_sdkCaptureMcpApp, CaptureMcpCallContext, add_capture_mcp_tools, capture_app_identity and the embeddings client.
capture_mcp_sdk.typesCaptureMcpSettings and CaptureAppSettings.
capture_mcp_sdk.authThe identity building blocks, without any gRPC. See Authentication.
capture_mcp_sdk.testingIn-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.