Skip to content

Instantly share code, notes, and snippets.

@ochafik
Last active October 10, 2025 12:37
Show Gist options
  • Select an option

  • Save ochafik/5774e6862ea0cdf6a76e4d99ce48da06 to your computer and use it in GitHub Desktop.

Select an option

Save ochafik/5774e6862ea0cdf6a76e4d99ce48da06 to your computer and use it in GitHub Desktop.
Callables for MCP TypeScript SDK
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import {
Tool,
ListToolsResult,
CallToolResultSchema,
ToolListChangedNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
class CallableTool {
constructor(private tool: Tool, private client: Client) {}
async call(input: Record<string, unknown>, options?: RequestOptions) {
// Server validates input, client validates output automatically
return await this.client.callTool(
{ name: this.tool.name, arguments: input },
CallToolResultSchema,
options
);
}
// Expose tool metadata for reference
get name() { return this.tool.name; }
get description() { return this.tool.description; }
get inputSchema() { return this.tool.inputSchema; }
get outputSchema() { return this.tool.outputSchema; }
}
async function* lazilyBuildCallableToolCache(client: Client, cache: Map<string, CallableTool | undefined>, options?: RequestOptions): AsyncGenerator<CallableTool> {
let cursor: string | undefined = undefined;
do {
const result = await client.listTools({ cursor }, options);
for (const tool of result.tools) {
const callable = new CallableTool(tool, client);
cache.set(tool.name, callable);
yield callable;
}
cursor = result.nextCursor;
} while (cursor);
}
export class CallableTools {
private cache = new Map<string, CallableTool | undefined>();
private cacheBuilder?: AsyncGenerator<CallableTool>
constructor(private client: Client) {}
notifyToolListChanged() {
this.cache.clear();
this.cacheBuilder = undefined;
}
async find(name: string): Promise<CallableTool | undefined> {
if (this.cache.has(name)) {
return this.cache.get(name);
}
if (!this.cacheBuilder) {
this.cacheBuilder = lazilyBuildCallableToolCache(this.client, this.cache);
}
while (true) {
const { value: tool, done } = await this.cacheBuilder.next();
if (done) {
break;
}
if (tool.name === name) {
return tool;
}
}
this.cache.set(name, undefined);
return undefined;
}
}
async function main() {
console.log("[callables]: Starting...");
const client = new Client({
name: 'Callables Client',
version: '0.1.0',
});
client.setNotificationHandler(ToolListChangedNotificationSchema, () => callables.notifyToolListChanged());
const callables = new CallableTools(client);
const [toolName, toolArgs, command, ...args] = process.argv.slice(2);
console.log(JSON.stringify({toolName, toolArgs, command, args}, null, 2));
const transport = new StdioClientTransport({command, args});
await client.connect(transport);
const tool = await callables.find(toolName);
if (!tool) {
console.error(`[callables]: Tool ${toolName} not found`);
process.exit(1);
}
console.log(`[callables]: Calling tool ${toolName}`);
console.log(await tool?.call(JSON.parse(toolArgs)));
await client.close();
}
main().catch((error) => {
console.error("[callables]: Fatal error:", error);
process.exit(1);
});

This example shows how to find tools by name and call them on the fly.

const callables = new CallableTools(client);
const echoTool = await callables.find('echo');
echoTool!.call({message: 'Hey'})

Usage:

docker run --rm node:latest \
  npx -y https://gist.github.com/ochafik/5774e6862ea0cdf6a76e4d99ce48da06 \
    echo \
    '{"message": "Heya"}' \
    npx -y --silent @modelcontextprotocol/server-everything

Note: inspect the tools available using:

npx -y @modelcontextprotocol/inspector npx -- -y --silent @modelcontextprotocol/server-everything
{
"name": "mcp-callable-tools",
"version": "1.0.0",
"description": "",
"license": "ISC",
"author": "",
"type": "module",
"bin": "dist/index.js",
"scripts": {
"prepare": "npm run build",
"start": "bun run index.ts",
"build": "bun build index.ts --outdir=dist --banner '#!/usr/bin/env node' --target=node --minify && shx chmod +x dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.19.1",
"zod-from-json-schema": "^0.5.0"
},
"devDependencies": {
"bun": "^1.2.23",
"shx": "^0.4.0"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment