Skip to content

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.

  • Authoring a per-server tools allowlist. You need the real, original tool names a server exposes before you can write { tools: { search: true } } correctly — listMcpTools is 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.json editor. Show the user what a server offers before they enable it in the running agent.

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.

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 })
}
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.
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.
  • 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.