listMcpTools
JavaScript · package toolnexus · SPEC §2 Gap 6 · js/src/mcp.ts
function listMcpTools(input: string | object, opts?: { signal?: AbortSignal }): Promise<McpInventory>
interface ToolInfo { name: string description: string inputSchema: JSONSchema}
interface McpInventory { tools: Record<string, ToolInfo[]> status: Record<string, McpStatus>}Connects to every enabled server just long enough to list its tools, then disconnects — no
Tool[] you can execute, no toolkit, nothing left running. It answers “what would this config give
me” without any of the commitment of loadMcp.
When to use it
Section titled “When to use it”- Authoring a per-server
toolsallowlist. You need the real, original tool names a server exposes before you can write{ tools: { search: true } }correctly —listMcpToolsis unfiltered by that allowlist for exactly this reason. - A dry-run / doctor command. “Which of my configured servers actually come up, and what would they add?” without executing anything or holding a connection open.
- Building UI for an
mcp.jsoneditor. Show the user what a server offers before they enable it in the running agent.
Why this and not the alternative
Section titled “Why this and not the alternative”The other difference from loadMcp is filtering: McpInventory.tools is always the full,
unfiltered list, keyed by each tool’s original (unprefixed) name — even if the server’s
config carries a tools allowlist. That is deliberate: this is the function you use to write
the allowlist, so it has to show you everything there is to choose from.
Examples
Section titled “Examples”1. The smallest useful call — an inline, disabled-only config
Section titled “1. The smallest useful call — an inline, disabled-only config”No connection happens at all; listMcpTools still reports the status.
import assert from "node:assert"import { listMcpTools } from "toolnexus"
const inv = await listMcpTools({ search: { type: "remote", url: "https://example.com/mcp", enabled: false },})
assert.deepEqual(inv.status, { search: "disabled" })assert.deepEqual(inv.tools, {})
console.log("ok:", JSON.stringify(inv.status))2. A real, hermetic listing — unfiltered names, then disconnected
Section titled “2. A real, hermetic listing — unfiltered names, then disconnected”A real stdio MCP server (the same tiny toolnexus-built server used on the loadMcp
page) exposing three tools, listed with a per-server allowlist present
in the config — and ignored, because inventory is always unfiltered.
import assert from "node:assert"import fs from "node:fs"import path from "node:path"import { listMcpTools } from "toolnexus"
const serverScript = `import { buildMcpServer, defineTool } from "toolnexus"import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"const tools = ["a", "b", "c"].map((n) => defineTool({ name: n, description: n, run: () => n }))const server = buildMcpServer(tools, { name: "docs-demo" })await server.connect(new StdioServerTransport())`const scriptPath = path.resolve(process.cwd(), "js", `_docs_mcp_list_demo_${Date.now()}.mjs`)fs.writeFileSync(scriptPath, serverScript)
try { const inv = await listMcpTools({ // A `tools` allowlist here narrows what loadMcp would expose — listMcpTools ignores it. srv: { type: "local", command: ["node", scriptPath], tools: { a: true } }, bad: { type: "local", command: ["node", "/no/such/script.mjs"] }, })
assert.deepEqual(inv.tools.srv.map((t) => t.name).sort(), ["a", "b", "c"], "unfiltered, original names") assert.equal(inv.status.srv, "connected") assert.equal(inv.status.bad, "failed") assert.equal(inv.tools.bad, undefined, "a failed server contributes no entry")
console.log("ok:", inv.tools.srv.map((t) => t.name).join(","), "|", JSON.stringify(inv.status))} finally { fs.rmSync(scriptPath, { force: true })}3. The full surface — full ToolInfo shape and a bounding signal
Section titled “3. The full surface — full ToolInfo shape and a bounding signal”Each ToolInfo carries name, description, and inputSchema — enough to render a picker UI or
validate an allowlist entry against real argument shapes, all bounded by opts.signal.
import assert from "node:assert"import fs from "node:fs"import path from "node:path"import { listMcpTools } from "toolnexus"
const serverScript = `import { buildMcpServer, defineTool } from "toolnexus"import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"const tools = [ defineTool({ name: "search", description: "Search the docs", inputSchema: { type: "object", properties: { q: { type: "string" } }, required: ["q"] }, run: ({ q }) => "results for " + q, }),]const server = buildMcpServer(tools, { name: "docs-demo" })await server.connect(new StdioServerTransport())`const scriptPath = path.resolve(process.cwd(), "js", `_docs_mcp_list_full_demo_${Date.now()}.mjs`)fs.writeFileSync(scriptPath, serverScript)
try { const inv = await listMcpTools( { srv: { type: "local", command: ["node", scriptPath] } }, { signal: AbortSignal.timeout(10_000) }, )
const info = inv.tools.srv[0] assert.equal(info.name, "search") assert.equal(info.description, "Search the docs") assert.deepEqual(info.inputSchema.required, ["q"]) assert.equal(inv.status.srv, "connected")
console.log("ok:", info.name, JSON.stringify(info.inputSchema))} finally { fs.rmSync(scriptPath, { force: true })}Options
Section titled “Options”| Field | Type | What it does |
|---|---|---|
input |
string | object |
Same accepted shapes as parseMcpConfig. |
opts.signal |
AbortSignal |
Bounds the entire listing. Firing it rejects the in-flight call. |
What you get back
Section titled “What you get back”| Field | Type | |
|---|---|---|
tools |
Record<string, ToolInfo[]> |
Per-server, unfiltered, original tool names — no server_tool prefix, no tools allowlist applied. |
status |
Record<string, McpStatus> |
"connected", "disabled", or "failed" per server name. A failed/disabled server has no entry in tools. |
See also
Section titled “See also”loadMcp— Read an mcp.json, connect every local stdio and remote streamable-HTTP server, expose each server tool as a Tool.loadMcp— The ctx-aware load: bound connection time and cancel a slow or hung server without leaking a child process.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.