|
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); |
|
}); |