@nightowlsdev/mcp
Adapter/ToolingMastraConnect external MCP servers as Night Owls tools, approval-gated, output-fenced, and engine-wall-safe.
What it does
@nightowlsdev/mcp wraps a Mastra MCPClient and exposes each discovered MCP (Model Context Protocol) tool as a Night Owls SwarmTool, so you grant external MCP tools to agents exactly like any other skill. createMcpConnector takes a map of stdio ({command,args}) or http/SSE ({url}) servers (each with an optional credentialRef) and a SecretResolver; connector.listTools(ctx) returns executable SwarmTools and connector.dispose() tears down the client. It layers four guarantees over a raw MCP client: tools default to needsApproval (origin mcp), untrusted output is wrapped in an <mcp-tool-output untrusted> fence to contain prompt injection, credentials resolve at execute time scoped to the live context, and disposal is explicit. FR-056 §9 adds an OPT-IN structured seam beside the fence: `callToolStructured(serverKey, toolName, input, ctx)` returns schema-validated typed data (`{ data: T }`) for a DATA consumer (a task provider) instead of the untrusted `{ fenced }` string — default-OFF, gated per-server-per-tool by the host's in-code `McpServerDef.structuredTools` Zod allowlist (a live function, never rebuildable from a persisted `mcp_servers` row; the DB carries only a `structured_profile` name that `structuredToolsFromProfile` maps to it), server-scoped so a colliding tool name on a co-located un-opted server cannot cross the opt-in; every un-opted server stays byte-identical to the fenced path. The @mastra/mcp dependency is imported lazily and the Mastra Tool type never reaches the public API, so dist/index.d.ts is @mastra-free, but the package does depend on @mastra/* at runtime. It also exports a CLI plugin manifest so `owl install mcp` can wire it.
Install
pnpm add @nightowlsdev/mcpKey exports
- createMcpConnector
- fenceToolOutput
- callToolStructured / structuredToolsFromProfile (FR-056 §9: the opt-in un-fenced structured-output seam)
- McpStructuredNotAllowed / McpStructuredInvalid
- nightOwlsPlugin (CLI manifest)
- types: McpConnector, McpConnectorOpts, McpServerDef (+ structuredTools), McpTool
Usage
import { createMcpConnector } from "@nightowlsdev/mcp";
const connector = createMcpConnector(
{ filesystem: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "."] } },
secretResolver,
);
const tools = await connector.listTools(ctx); // MCP tools as Night Owls SwarmTools
// ... grant `tools` to an agent ...
await connector.dispose();What it provides
mcp connects EXTERNAL MCP (Model Context Protocol) servers into a Night Owls swarm as first-class tools. `createMcpConnector({ servers, secrets })` takes a map of stdio ({ command, args }) or http/SSE ({ url }) servers, and `connector.listTools(ctx)` returns the discovered tools as executable Night Owls SwarmTools you grant to agents exactly like any other skill. It layers four guarantees over a raw MCP client: tools default to approval-gated, untrusted output is fenced against prompt injection, credentials resolve at execute time scoped to the live context, and the client is disposed explicitly.
When to use it
- You want an agent to call tools that already live behind an MCP server — a filesystem server, a hosted SaaS MCP endpoint, an internal service — without re-authoring them as Night Owls tools.
- You need third-party tool output treated as untrusted: fenced before it reaches the model, and gated for human approval by default.
- You want per-tenant credentials resolved when the tool runs (and re-resolved on a durable resume), not baked in at construction.
- You are wiring a mix of stdio (local process) and http/SSE (remote) MCP servers under one connector.
When not to
- The capability is a plain HTTP API you control — declare it with @nightowlsdev/connectors' `defineConnector` instead; you skip the MCP protocol and get the same approval/fence model over a portable op DSL.
- You want to EXPOSE your swarm to other MCP clients rather than consume external tools — that is the inverse package, @nightowlsdev/mcp-server.
- You need a UI-editable catalog of stdio servers — that is a remote-code-execution surface and is blocked by design (persisted mcp_servers rows are HTTP-only; stdio servers must stay code-defined).
Alternatives
- @nightowlsdev/connectorsThe external service is a REST/HTTP API. `defineConnector` declares its actions as validated data and materializes the same approval-gated, output-fenced SwarmTools — without an MCP server in between, and with pluggable per-org credential backends.
- A raw MCP client (@mastra/mcp / the MCP SDK) directlyYou want the raw tool results with no Night Owls trust layer — no default approval, no untrusted fence, no execute-time secret resolution. You take on containing prompt injection yourself.
Strengths
- Turns any MCP tool into a grantable SwarmTool — the agent sees it as an ordinary skill, so nothing about your agent code changes.
- Safe by default: origin 'mcp' means needsApproval defaults to true, and every result is wrapped in an <mcp-tool-output untrusted> fence to contain prompt injection from third-party output.
- Credentials never bake in: a credentialRef resolves through your SecretResolver at execute time, scoped to the run's context, and re-resolves on a durable resume.
- Engine-wall clean at the surface: @mastra/mcp is imported lazily and its Tool type never reaches the public API, so dist/index.d.ts is @mastra-free (though the package does depend on @mastra/* at runtime).
Limits & trade-offs
- It carries a runtime dependency on @mastra/mcp (and @mastra/core) — it is @mastra-free only in its public .d.ts, not at runtime, unlike connectors which depends on nothing but core + zod.
- You must call `connector.dispose()` in a finally at the run/connector boundary — the underlying client is a live process/connection, and forgetting to dispose leaks it.
- Persisted (UI-editable) MCP servers are HTTP-only by CHECK constraint; stdio servers ({ command, args }) cannot be admin-created and must be passed in code, because this client executes that definition.
- The default fenced path returns an untrusted string, not typed data — a DATA consumer needs the opt-in structured seam (callToolStructured), which you must explicitly allowlist per server per tool.
How it works
createMcpConnector wraps a lazily-constructed Mastra MCPClient. On listTools(ctx) it discovers each server's tools and wraps every one into an McpTool — a SwarmTool that is also directly executable and returns { fenced }. A namespaced tool name is attributed to its server by exact prefix match, longest key first, so a short server name that prefixes a longer one cannot claim the longer one's tools. At execute time the tool resolves its credentialRef through the injected SecretResolver scoped to the live context, calls through, and wraps the raw result in the <mcp-tool-output untrusted="true"> envelope before returning it. FR-056 §9 adds an opt-in structured seam beside the fence: callToolStructured(serverKey, toolName, input, ctx) returns schema-validated { data } for a data consumer — default-off, gated per-server-per-tool by an in-code Zod allowlist (structuredToolsFromProfile maps a persisted profile name to the live functions), server-scoped so a colliding tool name on a co-located un-opted server cannot cross the opt-in.
Examples
Connect stdio + http servers, grant the tools, dispose
Each discovered tool becomes an approval-gated, output-fenced SwarmTool. ALWAYS dispose in a finally.
import { createMcpConnector } from "@nightowlsdev/mcp";
import { defineAgent } from "@nightowlsdev/core";
const connector = createMcpConnector({
servers: {
files: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"] },
calendar: { url: new URL(process.env.MCP_CALENDAR_URL), credentialRef: "calendar-token" },
},
secrets: mySecretResolver, // SecretResolver.resolve(ref, ctx), resolved at execute time
});
try {
const tools = await connector.listTools(ctx); // McpTool[], origin "mcp", needsApproval true
const agent = defineAgent({ slug: "assistant", personality: "Helpful.", skills: tools });
// ...run the swarm with this agent...
} finally {
await connector.dispose(); // tear down the underlying client
}Opt in to typed structured output (FR-056 §9)
For a DATA consumer that needs validated fields, not the untrusted fenced string — allowlisted per server per tool.
import { createMcpConnector, McpStructuredNotAllowed } from "@nightowlsdev/mcp";
import { z } from "zod";
const connector = createMcpConnector({
servers: {
weather: {
url: new URL(process.env.MCP_WEATHER_URL),
// an in-code Zod allowlist — never rebuildable from a persisted DB row
structuredTools: { get_forecast: z.object({ tempC: z.number() }) },
},
},
secrets: mySecretResolver,
});
// Returns { data } for the allowlisted tool; throws McpStructuredNotAllowed for any un-opted one.
const { data } = await connector.callToolStructured("weather", "get_forecast", { city: "NYC" }, ctx);Doing the parts it doesn't support
- A UI-managed catalog of stdio serversNot allowed — a UI-editable row carrying command/args is a remote-code-execution surface. The persisted mcp_servers table (storage-supabase 0025, served by runner-nextjs connectionsRoutes) is HTTP-only by CHECK constraint. Keep stdio servers code-defined, passed in `servers`, where they get code review and deploy-time control.
- Getting typed data out of a tool you did not opt inThe default path fences output into an untrusted string on purpose. To get { data }, add the tool to that server's `structuredTools` Zod allowlist (or map a persisted profile name via structuredToolsFromProfile) and call callToolStructured — it is server-scoped and default-off, and throws McpStructuredNotAllowed / McpStructuredInvalid otherwise.
Related
- mcp-server — The inverse: expose YOUR swarm to other MCP clients as ask_<agent> tools.
- connectors — The sibling tool-source seam for plain HTTP APIs — same approval + fence model, no MCP server needed.
- core — SwarmTool, SwarmContext, and the SecretResolver seam the connector materializes against.
- approval-modes — How the tool risk vector and approval gate that MCP tools default into actually compose.
- cli — `owl install mcp` scaffolds the env key + config guidance for wiring an MCP server.