v0.1.0GitHubnpm
Concepts

MCP servers

Mount any Model Context Protocol server as a tool group. Skawld launches the server as a child process (stdio) or connects over Streamable HTTP, lists its tools, and exposes each one to the agent under an mcp__<server>__<tool> name.

mcp.ts
import { Agent } from "@skawld/agent-sdk";

const agent = new Agent({
  provider,
  model: "claude-opus-4-5",
  mcpServers: {
    postgres: {
      type: "stdio",
      command: "uvx",
      args: ["mcp-server-postgres", process.env.DATABASE_URL!],
    },
  },
});

#Configuring servers

Pass mcpServers on Agent as a record keyed by server name. Each entry is either an McpStdioServerConfig (spawn a child process and talk over stdio) or an McpHttpServerConfig (connect to a remote server over Streamable HTTP). The shape mirrors the Claude Agent SDK’s mcpServers option so existing configs port across without translation.

#Stdio servers

Stdio is the default transport. The server is spawned as a child process; JSON-RPC flows over its stdin/stdout. Child stderr is inherited so the server’s diagnostics appear on your console and a chatty server can never fill an unread pipe and block.

FieldTypePurpose
type"stdio" (optional)Discriminator. Omit it and stdio is assumed; include it for symmetry with HTTP configs.
commandstringExecutable to spawn. Resolved against PATH — pass an absolute path if you want to avoid that.
argsstring[] (optional)Arguments passed to command. Defaults to [].
envRecord<string, string> (optional)Extra environment variables, merged over the parent process env. Keys you set here win against the inherited value.

Three idiomatic stdio launchers — Python via uvx, Node via npx, and a local script — look like this:

stdio-servers.ts
mcpServers: {
  // Python MCP server pulled and run by uvx
  postgres: {
    command: "uvx",
    args: ["mcp-server-postgres"],
    env: { DATABASE_URL: process.env.DATABASE_URL! },
  },
  // Node MCP server, downloaded on demand by npx
  filesystem: {
    type: "stdio",
    command: "npx",
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/srv/data"],
  },
  // Your own local MCP server, run by an interpreter you control
  internal: {
    command: "/usr/bin/python3",
    args: ["./tools/mcp_server.py"],
  },
},

Secrets belong in env, not args.

Arguments are visible to anyone who can ps the host; environment variables given to a child process are not. Pass DATABASE_URL, OPENAI_API_KEY, etc. via env and let the MCP server read them.

#HTTP servers

HTTP is Streamable HTTP — the modern MCP transport that multiplexes JSON-RPC over a single HTTP connection. Use it for hosted MCP servers or anything running outside your process.

FieldTypePurpose
type"http"Required discriminator.
urlstringServer endpoint URL. Parsed with the platform URL constructor — invalid URLs fail at connect time.
headersRecord<string, string> (optional)Static headers sent with every request. Use for Authorization, tenant ids, or anything else the server expects on each call.
http-server.ts
mcpServers: {
  docs: {
    type: "http",
    url: "https://mcp.example.com/v1",
    headers: {
      Authorization: `Bearer ${process.env.MCP_TOKEN!}`,
      "X-Tenant": "acme",
    },
  },
},

#Choosing a transport

Two questions decide it: where does the server run, and who owns its credentials?

Use stdio when…Use HTTP when…
The server is a local binary or script you can spawn — file system, local databases, ad-hoc Python/Node tools.The server is hosted (yours or a vendor’s) and shared across many agents, or its dependencies are too heavy to spawn per-process.
Secrets live in your process environment and you want to hand them to the child via env.Auth is a token you pass over the wire as a header — the agent never sees the underlying credentials.
You want full lifecycle control: the process dies when the agent closes.The server has its own lifecycle (deploy, scale, monitor) and you only want a client connection.

#Multiple servers

Configure as many servers as you want — they connect in parallel and their tools merge into the same registry. The config keys become the namespace prefix, so duplicate tool names across servers never collide.

multi-mcp.ts
import { Agent } from "@skawld/agent-sdk";

const agent = new Agent({
  provider,
  model: "claude-opus-4-5",
  mcpServers: {
    postgres: {
      command: "uvx",
      args: ["mcp-server-postgres"],
      env: { DATABASE_URL: process.env.DATABASE_URL! },
    },
    github: {
      type: "http",
      url: "https://mcp.github.com",
      headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN!}` },
    },
  },
});

#Connection lifecycle

MCP servers do not connect when you construct the Agent. They connect lazily on the first agent.session() call and the connect attempt is memoized — concurrent session() calls share one in-flight attempt rather than racing.

On agent.close() every client is closed; stdio child processes are terminated. close() is idempotent.

Connect is fail-fast. If any configured server fails to connect or fails its initial listTools call, every server that had already opened is torn down and connectMcpServers throws a ConfigError aggregating the failures. A partial set is never returned. The memoized attempt is cleared on failure, so the next agent.session() call will retry from scratch.

Per-call errors after a successful connect are handled differently: a tool invocation that throws is captured and returned as a tool result with is_error: true, so the model sees the failure and can react rather than crashing the run.

#Tool naming

Every MCP tool is registered under the fully-qualified name mcp__<server>__<tool>. The server segment is your config key; the tool segment is the name the MCP server reports in its own manifest. The fixed mcp__ prefix guarantees MCP tools never collide with skawld built-ins (Read, Bash, etc.).

Both segments are normalised: any character outside [a-zA-Z0-9_-] is replaced with a single underscore. So a config key of my server.v2 becomes my_server_v2 and a tool named fetch:url becomes fetch_url.

Config keyMCP tool nameExposed as
postgresquerymcp__postgres__query
githublist-issuesmcp__github__list-issues
my server.v2fetch:urlmcp__my_server_v2__fetch_url

Collision detection. If two config keys normalise to the same string (e.g. my.server and my-server both become my_server), connectMcpServers throws a ConfigError before any process is spawned. Rename one key.

#Permissions

MCP tools flow through the same permission engine as every other tool, matched by their fully-qualified name. To allow or deny a specific MCP tool, use the mcp__<server>__<tool> form in a ToolRule. See Permissions for rule semantics and the canUseTool callback.

mcp-permissions.ts
const agent = new Agent({
  provider,
  model: "claude-opus-4-5",
  mcpServers: {
    postgres: { command: "uvx", args: ["mcp-server-postgres"] },
  },
  permissions: {
    rules: [
      // Pre-approve read-only queries
      { kind: "tool", tool: "mcp__postgres__query", decision: "allow" },
      // Block the destructive one outright
      { kind: "tool", tool: "mcp__postgres__exec", decision: "deny" },
    ],
  },
});

#How MCP tools behave

Each MCP tool is wrapped as a skawld Tool at connect time. The wrapper preserves the server’s name, description, and JSON input schema verbatim — the MCP server owns its own validation, so skawld only checks that input is an object and passes it through.

PropertyDerived from
scope"read" when the MCP tool sets the readOnlyHint annotation; "write" otherwise.
parallelSafeSame signal — read-only MCP tools can run in parallel; the rest are serialised by the scheduler.
input_schemaThe MCP tool’s inputSchema (properties + required) passed through unchanged.
Result contentMCP text blocks become skawld text; image blocks become base64 image blocks. Audio and embedded-resource blocks degrade to a text placeholder.

Tools only, for now. Skawld mounts MCP tools. MCP resources and prompts are not exposed to the agent today — only what the server returns from listTools is wired in.