loadMcp
JavaScript · package toolnexus · SPEC §2 · js/src/mcp.ts
function loadMcp( input: string | object, opts?: { waitFor?: (request: Request) => Promise<Answer>; signal?: AbortSignal }): Promise<McpSource>
interface McpSource { tools: Tool[] status: Record<string, McpStatus> close(): Promise<void>}Parses an mcp.json (via parseMcpConfig) and connects to
every enabled server — local stdio child processes and remote streamable-HTTP endpoints alike —
in parallel. Each listed tool comes back as a uniform Tool, namespaced server_tool so two
servers exposing search don’t collide.
When to use it
Section titled “When to use it”- You are wiring MCP servers into your own loop, outside a
Toolkit— you want theTool[]and the connection lifecycle directly. - You need
mcpStatus()-style visibility (which servers connected, which failed) without paying for skills, built-ins, or the rest ofcreateToolkit’s aggregation. - You are handling MCP elicitation yourself and want to pass
waitForstraight through, without a toolkit in between.
Why this and not the alternative
Section titled “Why this and not the alternative”Failures are isolated per server — one broken server never stops the others from connecting.
That isolation is the reason loadMcp returns a status map instead of throwing on the first bad
config: a caller building a UI can show “3 of 4 servers connected” instead of a blank error.
Examples
Section titled “Examples”1. The smallest useful call — nothing actually connects
Section titled “1. The smallest useful call — nothing actually connects”An inline config with everything disabled. No child process, no HTTP request — loadMcp still
does the parse + status bookkeeping, which is often all you need to test against.
import assert from "node:assert"import { loadMcp } from "toolnexus"
const mcp = await loadMcp({ search: { type: "remote", url: "https://example.com/mcp", enabled: false },})
// Nothing connected — the disabled server contributes zero tools.assert.deepEqual(mcp.tools, [])assert.deepEqual(mcp.status, { search: "disabled" })
// close() is always safe to call, even with nothing to shut down.await mcp.close()
console.log("ok:", JSON.stringify(mcp.status))2. A real, hermetic connection — no network required
Section titled “2. A real, hermetic connection — no network required”loadMcp connecting over stdio to an actual MCP server, side by side with the shared
examples/mcp.json fixture’s disabled remote entry. The server here is a few lines of toolnexus
itself (buildMcpServer + defineTool), spawned as a child process — the same pattern toolnexus’s
own test suite uses to avoid depending on the network or npx fetching a package in CI. (A real
project just keeps this as its own server.mjs file next to its own @modelcontextprotocol/sdk
dependency — writing it to a temp path here is only to keep this example self-contained.)
import assert from "node:assert"import fs from "node:fs"import path from "node:path"import { loadMcp, parseMcpConfig } from "toolnexus"
// A tiny real MCP server, written next to toolnexus's own source so it can resolve// both `toolnexus` and `@modelcontextprotocol/sdk`, and run over stdio.const serverScript = `import { buildMcpServer, defineTool } from "toolnexus"import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"const tools = [ defineTool({ name: "greet", description: "Greet by name", run: (args) => "Hello, " + args.name + "!" }),]const server = buildMcpServer(tools, { name: "docs-demo" })await server.connect(new StdioServerTransport())`const scriptPath = path.resolve(process.cwd(), "js", `_docs_mcp_load_demo_${Date.now()}.mjs`)fs.writeFileSync(scriptPath, serverScript)
try { // examples/mcp.json's "example-remote" is enabled:false — inspect it without connecting. const fixture = parseMcpConfig("examples/mcp.json") assert.equal(fixture["example-remote"].enabled, false)
const mcp = await loadMcp({ greeter: { type: "local", command: ["node", scriptPath] }, remote: fixture["example-remote"], })
assert.deepEqual(mcp.status, { greeter: "connected", remote: "disabled" }) assert.deepEqual(mcp.tools.map((t) => t.name), ["greeter_greet"])
const res = await mcp.tools[0].execute({ name: "Muthu" }) assert.equal(res.output, "Hello, Muthu!")
await mcp.close() console.log("ok:", JSON.stringify(mcp.status))} finally { fs.rmSync(scriptPath, { force: true })}3. The full surface — waitFor, signal, and a failing server isolated
Section titled “3. The full surface — waitFor, signal, and a failing server isolated”opts.signal bounds the whole load; opts.waitFor is the §10 resolver an MCP server can call
mid-tools/call to ask the host a question. A server with a broken command fails without taking
the others down with it.
import assert from "node:assert"import { loadMcp } from "toolnexus"
const waitFor = async (request: { prompt: string }) => ({ id: "a1", ok: true, data: { value: "approved" } })
const mcp = await loadMcp( { broken: { type: "local", command: ["node", "/no/such/script.mjs"] }, off: { type: "remote", url: "https://example.com/mcp", disabled: true }, }, { waitFor, signal: AbortSignal.timeout(5_000) },)
// The broken server is isolated as "failed" — parsing/loading the others is unaffected.assert.equal(mcp.status.broken, "failed")assert.equal(mcp.status.off, "disabled")assert.deepEqual(mcp.tools, [])
await mcp.close()console.log("ok:", JSON.stringify(mcp.status))Options
Section titled “Options”| Field | Type | What it does |
|---|---|---|
input |
string | object |
Same accepted shapes as parseMcpConfig — a path or a parsed config object. |
opts.waitFor |
(req: Request) => Promise<Answer> |
Resolver for MCP elicitation (§10). Omit ⇒ elicitation capability is not advertised to the server. |
opts.signal |
AbortSignal |
Bounds the entire load. Firing it rejects the in-flight loadMcp call. |
What you get back
Section titled “What you get back”| Field | Type | |
|---|---|---|
tools |
Tool[] |
Flattened across every connected server, prefixed server_tool. |
status |
Record<string, McpStatus> |
"connected", "disabled", or "failed" per server name. |
close() |
() => Promise<void> |
Shuts down every connected server’s transport. Always call it. |
See also
Section titled “See also”loadMcp— The ctx-aware load: bound connection time and cancel a slow or hung server without leaking a child process.listMcpTools— List what each configured server would expose, plus per-server status, without wiring it into a toolkit.parseMcpConfig— Parse and validate config without connecting — the fast fail for a malformed or misspelled server block.elicitationToRequest— Map an MCP server’s elicitation request onto the §10 suspension contract, and map the answer back.