Skip to main content

The shape of it

connect_mcp(name, command, args, capability?) spawns an MCP server as a subprocess over stdio, discovers its tools once at connection time, and returns an McpServer value (.name, .tools, .close()). Each tool in .tools reports as "Tool" from type_of() and can be handed to ask_with_tools exactly like a Sense tool — that’s the only way to invoke one; there’s no direct call syntax. Requires the optional mcp package (pip install sense-lang[mcp], imported lazily — importing sense_lang itself never requires it).

When to use this

Reach for connect_mcp when the capability you want to expose to a model already exists as an MCP server — someone else’s filesystem tool, database tool, or API wrapper — rather than reimplementing it as a Sense tool. Use a plain tool instead when the logic is simple enough to write directly in Sense, or when you need Sense-level type checking on the parameters.

An MCP tool has no Sense body — invoking it is a real RPC call

This is the one place in Sense’s interop story that needed genuinely new runtime infrastructure rather than composition of what already existed: an MCP tool’s implementation lives in another process, so calling one means a JSON-RPC request over the official mcp SDK, not running a ToolDecl’s body.
1

No direct call syntax

An McpTool never gets a Sense identifier the way tool search(...) does. MCP tools exist for a model to discover and choose from via ask_with_tools (or a skill bundling them), not for a human to call by name in source.
2

One capability covers the whole connection

Not per tool — splitting that finely would mean guessing at an external server’s tool semantics from its name/description. Two servers needing different capabilities means two connect_mcp(...) calls. Tool names are namespaced server.tool so two servers (or a server and a bare Sense tool) can’t collide on a generic name.
3

close() is explicit

Unlike agent threads or SQLite connections elsewhere in Sense (no explicit cleanup needed, reclaimed at process exit), an MCP connection spawns a real OS subprocess — a more visible failure mode left uncleaned than an idle thread, so it gets its own cleanup call.
Invoking an MCP tool runs through a path parallel to (not through) the one a Sense tool call uses — same shape (capability check, audit entry, denial fed back to the model rather than raised).

Why a background thread, not a callback

An MCP session has to be reused across many calls, which rules out asyncio.run() per call — it would respawn the subprocess every time. The natural async-native alternative — open the connection via a callback (async with ... as tools: ...) — isn’t available either, since Sense has no anonymous function/closure syntax to pass one. A background thread running its own persistent asyncio event loop, bridged via asyncio.run_coroutine_threadsafe(...).result(...), is the option actually available given those two constraints.
One detail worth knowing if you’re debugging a connection issue: the connection’s entire lifetime — connect, every tool call, and shutdown — runs as a single long-lived task on that background thread’s event loop, rather than one task per operation. mcp’s own context managers use anyio cancel scopes internally, which require entering and exiting in the same asyncio task; splitting connect and shutdown into separate tasks raises a cancel-scope error from anyio itself.

What this doesn’t do (yet)

connect_mcp is stdio-only.
Only MCP tool discovery/invocation is wired up — not MCP resources or prompt templates.
A dead connection surfaces a clear error on the next call rather than auto-reconnecting. One background thread per connection, not pooled.

Continue

Tool-Calling

The mechanism an MCP server’s tools are exposed through.

Skills

Bundling MCP tools alongside Sense tools under one description.

Installation

The [mcp] optional extra connect_mcp needs.