Introduction
Once your client is connected to a server, the first thing you need to do is discover what the server offers. MCP provides three discovery methods — listTools(), listResources(), and listPrompts() — that let your client learn about available capabilities at runtime. This dynamic discovery is what makes MCP powerful: your application adapts to whatever tools, data, and prompts the server exposes without hardcoding anything.
Key Concepts
- client.listTools(): Returns an object with a
toolsarray containing tool definitions (name, description, inputSchema). Each tool represents an action the LLM can invoke. - client.listResources(): Returns an object with a
resourcesarray containing resource definitions (uri, name, mimeType, description). Each resource represents data the application can read. - client.listPrompts(): Returns an object with a
promptsarray containing prompt definitions (name, description, arguments). Each prompt is a reusable template. - Pagination with Cursors: When a server has many capabilities, list methods support pagination. The response includes a
nextCursorfield. Pass it as thecursorparameter in the next request to fetch the next page. - Capability Caching: After discovering capabilities, cache them locally. Re-fetch only when the server sends a
notifications/tools/list_changed(or equivalent) notification. - Elicitation Capability: A newer MCP capability that allows servers to request structured input from users via
elicitation/createrequests. Declared during initialization. - Tasks Capability: A newer MCP capability that enables tracking long-running operations as discrete tasks. Servers and clients can create, list, and cancel tasks.
Real World Context
You are building an AI assistant that connects to multiple MCP servers — a file system server, a database server, and a Slack server. When the assistant starts, it calls listTools() on each server to build a unified catalog of all available tools. It then converts these tool definitions into the format your LLM API expects (e.g., Anthropic function calling format) and passes them with every LLM request. When the user says "find the latest sales report and post it to Slack," the LLM can pick the right tools from any connected server.
Deep Dive
Listing Tools
The listTools() method returns all tools the server exposes:
typescriptconst toolsResult = await client.listTools(); for (const tool of toolsResult.tools) { console.error(`Tool: ${tool.name}`); console.error(` Description: ${tool.description}`); console.error(` Input Schema: ${JSON.stringify(tool.inputSchema)}`); }
Each tool in the array has three key fields:
textField Type Purpose ────────────────────────────────────────────────── name string Unique identifier for the tool description string Human/LLM-readable description inputSchema object JSON Schema defining accepted parameters
The inputSchema follows the JSON Schema standard, so your application can validate arguments before sending them to the server.
Listing Resources
The listResources() method returns data sources the server exposes:
typescriptconst resourcesResult = await client.listResources(); for (const resource of resourcesResult.resources) { console.error(`Resource: ${resource.name}`); console.error(` URI: ${resource.uri}`); console.error(` MIME type: ${resource.mimeType}`); }
Resources are identified by URIs and have MIME types that tell you what kind of data they contain (e.g., text/plain, application/json, image/png).
Listing Prompts
The listPrompts() method returns reusable prompt templates:
typescriptconst promptsResult = await client.listPrompts(); for (const prompt of promptsResult.prompts) { console.error(`Prompt: ${prompt.name}`); console.error(` Description: ${prompt.description}`); if (prompt.arguments) { for (const arg of prompt.arguments) { console.error(` Arg: ${arg.name} (required: ${arg.required})`); } } }
Prompt definitions include the template name, description, and any arguments the template accepts. The actual prompt content is retrieved later using client.getPrompt().
Handling Pagination
When a server has many capabilities, list methods return results in pages. The response includes a nextCursor field when there are more results:
typescriptasync function getAllTools(client: Client) { const allTools = []; let cursor: string | undefined; do { const result = await client.listTools(cursor ? { cursor } : undefined); allTools.push(...result.tools); cursor = result.nextCursor; } while (cursor); return allTools; }
This pattern fetches all pages by passing the nextCursor from each response as the cursor parameter of the next request. When nextCursor is undefined, all results have been retrieved.
Listening for Capability Changes
Servers can dynamically add or remove capabilities. If the server supports listChanged notifications, your client should listen for them and refresh its catalog:
typescriptclient.setNotificationHandler( "notifications/tools/list_changed", async () => { console.error("Tool list changed, refreshing..."); const result = await client.listTools(); // Update your cached tool list cachedTools = result.tools; } );
Similar notification handlers exist for resources (notifications/resources/list_changed) and prompts (notifications/prompts/list_changed). By listening for these notifications, your client always has an up-to-date view of the server's capabilities.
Building a Capability Catalog
In practice, you often want to build a catalog of all capabilities from all connected servers:
typescriptinterface ServerCapabilities { serverName: string; tools: Array<{ name: string; description: string; inputSchema: object }>; resources: Array<{ uri: string; name: string; mimeType?: string }>; prompts: Array<{ name: string; description: string }>; } async function discoverCapabilities( client: Client, serverName: string ): Promise<ServerCapabilities> { const [toolsResult, resourcesResult, promptsResult] = await Promise.all([ client.listTools(), client.listResources(), client.listPrompts() ]); return { serverName, tools: toolsResult.tools, resources: resourcesResult.resources, prompts: promptsResult.prompts }; }
Using Promise.all to fetch tools, resources, and prompts concurrently is more efficient than sequential calls, especially for remote servers where each call involves network latency.
Newer Capabilities: Elicitation and Tasks
Recent versions of the MCP specification have introduced additional capabilities beyond tools, resources, and prompts:
- Elicitation: Allows servers to request structured input from users. A server can send an
elicitation/createrequest to ask the user a question (via a form or URL redirect). The client declares support for this via theelicitationcapability during initialization. - Tasks: Enables long-running operations to be tracked as discrete tasks. Servers and clients can create, list, and cancel tasks via the
taskscapability. This is useful for operations that take significant time, like large data exports or complex analyses.
These capabilities are negotiated during the initialization handshake just like tools, resources, and prompts. Your client should check the server's capabilities response to see which features are available before attempting to use them.
Common Pitfalls
- Not handling empty capability lists — A server might expose tools but no resources or prompts. Always check for empty arrays rather than assuming all three categories are populated.
- Ignoring list_changed notifications — If a server adds or removes tools at runtime and your client does not listen for change notifications, the LLM will work with a stale tool catalog, leading to errors when it tries to call a removed tool.
- Fetching capabilities repeatedly — Calling
listTools()before every LLM request is wasteful. Fetch once at startup, cache the results, and only refresh when you receive a change notification.
Best Practices
- Cache capability lists at startup — Call all three list methods once after connection, store the results, and refresh only on change notifications.
- Use Promise.all for parallel discovery — Fetch tools, resources, and prompts concurrently. This reduces startup time, especially for remote servers.
- Tag capabilities with server name — When managing multiple servers, tag each tool and resource with the server it came from. This makes it easy to route calls to the correct client later.
Summary
client.listTools()returns tool definitions with name, description, and inputSchema.client.listResources()returns resource definitions with URI, name, and MIME type.client.listPrompts()returns prompt definitions with name, description, and arguments.- Pagination is handled via
nextCursor— loop until it is undefined to get all results. - Listen for
notifications/tools/list_changedand equivalent notifications to keep your catalog up to date. - Cache capabilities at startup and refresh only when the server signals a change.
- Newer capabilities like elicitation (server-initiated user input) and tasks (long-running operation tracking) are negotiated during initialization alongside tools, resources, and prompts.
Code Examples
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
// Discover all capabilities after connecting
const [toolsResult, resourcesResult, promptsResult] = await Promise.all([
client.listTools(),
client.listResources(),
client.listPrompts()
]);
console.error(`Tools: ${toolsResult.tools.length}`);
console.error(`Resources: ${resourcesResult.resources.length}`);
console.error(`Prompts: ${promptsResult.prompts.length}`);
// Listen for changes
client.setNotificationHandler(
"notifications/tools/list_changed",
async () => {
const updated = await client.listTools();
console.error(`Tool list updated: ${updated.tools.length} tools`);
}
);