src / tools-provider.ts
import { tool, Tool, ToolsProvider } from "@lmstudio/sdk";
import { z } from "zod";
import { MCPProxyClient, MCPTool, MCPToolParameter } from "./mcp-proxy";
import { DEFAULT_CONFIG, PluginConfig } from "./config";
let mcpClient: MCPProxyClient | null = null;
let startPromise: Promise<MCPProxyClient | null> | null = null;
async function getMCPClient(config: PluginConfig, workDir: string): Promise<MCPProxyClient | null> {
if (mcpClient && mcpClient.isReady()) return mcpClient;
// Prevent multiple concurrent starts
if (startPromise) return startPromise;
startPromise = (async () => {
const client = new MCPProxyClient(
config.pythonCommand,
config.serverPath,
workDir,
config.enableStdioLogging
);
return new Promise<MCPProxyClient | null>((resolve) => {
const timeout = setTimeout(() => {
console.error("[TW-Stock] MCP client start timeout (60s)");
client.removeAllListeners();
resolve(null);
}, 60000);
client.on("ready", () => {
clearTimeout(timeout);
mcpClient = client;
console.log("[TW-Stock] MCP client ready");
resolve(client);
});
client.on("error", (err: Error) => {
clearTimeout(timeout);
console.error("[TW-Stock] MCP client start failed:", err.message);
client.removeAllListeners();
resolve(null);
});
client.on("exit", () => {
clearTimeout(timeout);
console.log("[TW-Stock] MCP client exited");
mcpClient = null;
startPromise = null;
resolve(null);
});
client.start();
});
})();
return startPromise;
}
function zodTypeFromMCP(param: MCPToolParameter): z.ZodTypeAny {
let schema: z.ZodTypeAny;
switch (param.type) {
case "string":
schema = param.enum
? z.enum(param.enum as [string, ...string[]])
: z.string();
break;
case "number":
case "integer":
schema = param.type === "integer" ? z.number().int() : z.number();
break;
case "boolean":
schema = z.boolean();
break;
case "array":
schema = param.items
? z.array(zodTypeFromMCP(param.items as MCPToolParameter))
: z.array(z.any());
break;
case "object":
schema = z.record(z.any());
break;
default:
schema = z.any();
}
return schema.describe(param.description || "");
}
function buildZodSchema(mcpTool: MCPTool): Record<string, z.ZodTypeAny> {
const shape: Record<string, z.ZodTypeAny> = {};
const required = new Set(mcpTool.inputSchema.required || []);
for (const [name, param] of Object.entries(mcpTool.inputSchema.properties || {})) {
let schema = zodTypeFromMCP(param);
if (!required.has(name)) {
schema = param.default !== undefined ? schema.optional().default(param.default) : schema.optional();
}
shape[name] = schema;
}
return shape;
}
export const toolsProvider: ToolsProvider = async (ctl) => {
const context = ctl as any;
const pluginConfig = DEFAULT_CONFIG;
const workDir = context.pluginDir || context.dir || process.cwd();
console.log("[TW-Stock] Tools provider called, workDir:", workDir);
console.log("[TW-Stock] Config:", JSON.stringify(pluginConfig));
const mcp = await getMCPClient(pluginConfig, workDir);
if (!mcp) {
console.error("[TW-Stock] MCP client failed to start. No tools will be available.");
// Return a placeholder tool that reports the error
return [
tool({
name: "tw_stock_error",
description: "TW Stock MCP server is not available. Check logs for details.",
parameters: {
message: z.string().optional().describe("Your query (will return error message)"),
},
implementation: async () => {
return "❌ TWStock MCP Server is not running. " +
"Please ensure:\n" +
"1. Python is installed\n" +
"2. Run 'npm run setup-python' to install dependencies\n" +
"3. TWStockMCPServer/venv exists with Python installed";
},
}),
];
}
const mcpTools = mcp.getTools();
console.log(`[TW-Stock] Exposing ${mcpTools.length} MCP tools to LM Studio`);
return mcpTools.map((mcpTool: MCPTool) => {
const zodSchemaShape = buildZodSchema(mcpTool);
return tool({
name: mcpTool.name,
description: mcpTool.description || `MCP tool: ${mcpTool.name}`,
parameters: zodSchemaShape,
implementation: async (args: any) => {
console.log(`[TW-Stock] Calling tool: ${mcpTool.name}`);
try {
const result = await mcp.callTool(mcpTool.name, args);
if (result?.content) {
return result.content.map((c: any) => c.text || "").join("\n\n") || "(empty)";
}
return typeof result === "string" ? result : JSON.stringify(result);
} catch (error) {
const errMsg = (error as Error).message;
console.error(`[TW-Stock] Tool ${mcpTool.name} failed:`, errMsg);
return `Error: ${errMsg}`;
}
},
});
});
};src / tools-provider.ts
import { tool, Tool, ToolsProvider } from "@lmstudio/sdk";
import { z } from "zod";
import { MCPProxyClient, MCPTool, MCPToolParameter } from "./mcp-proxy";
import { DEFAULT_CONFIG, PluginConfig } from "./config";
let mcpClient: MCPProxyClient | null = null;
let startPromise: Promise<MCPProxyClient | null> | null = null;
async function getMCPClient(config: PluginConfig, workDir: string): Promise<MCPProxyClient | null> {
if (mcpClient && mcpClient.isReady()) return mcpClient;
// Prevent multiple concurrent starts
if (startPromise) return startPromise;
startPromise = (async () => {
const client = new MCPProxyClient(
config.pythonCommand,
config.serverPath,
workDir,
config.enableStdioLogging
);
return new Promise<MCPProxyClient | null>((resolve) => {
const timeout = setTimeout(() => {
console.error("[TW-Stock] MCP client start timeout (60s)");
client.removeAllListeners();
resolve(null);
}, 60000);
client.on("ready", () => {
clearTimeout(timeout);
mcpClient = client;
console.log("[TW-Stock] MCP client ready");
resolve(client);
});
client.on("error", (err: Error) => {
clearTimeout(timeout);
console.error("[TW-Stock] MCP client start failed:", err.message);
client.removeAllListeners();
resolve(null);
});
client.on("exit", () => {
clearTimeout(timeout);
console.log("[TW-Stock] MCP client exited");
mcpClient = null;
startPromise = null;
resolve(null);
});
client.start();
});
})();
return startPromise;
}
function zodTypeFromMCP(param: MCPToolParameter): z.ZodTypeAny {
let schema: z.ZodTypeAny;
switch (param.type) {
case "string":
schema = param.enum
? z.enum(param.enum as [string, ...string[]])
: z.string();
break;
case "number":
case "integer":
schema = param.type === "integer" ? z.number().int() : z.number();
break;
case "boolean":
schema = z.boolean();
break;
case "array":
schema = param.items
? z.array(zodTypeFromMCP(param.items as MCPToolParameter))
: z.array(z.any());
break;
case "object":
schema = z.record(z.any());
break;
default:
schema = z.any();
}
return schema.describe(param.description || "");
}
function buildZodSchema(mcpTool: MCPTool): Record<string, z.ZodTypeAny> {
const shape: Record<string, z.ZodTypeAny> = {};
const required = new Set(mcpTool.inputSchema.required || []);
for (const [name, param] of Object.entries(mcpTool.inputSchema.properties || {})) {
let schema = zodTypeFromMCP(param);
if (!required.has(name)) {
schema = param.default !== undefined ? schema.optional().default(param.default) : schema.optional();
}
shape[name] = schema;
}
return shape;
}
export const toolsProvider: ToolsProvider = async (ctl) => {
const context = ctl as any;
const pluginConfig = DEFAULT_CONFIG;
const workDir = context.pluginDir || context.dir || process.cwd();
console.log("[TW-Stock] Tools provider called, workDir:", workDir);
console.log("[TW-Stock] Config:", JSON.stringify(pluginConfig));
const mcp = await getMCPClient(pluginConfig, workDir);
if (!mcp) {
console.error("[TW-Stock] MCP client failed to start. No tools will be available.");
// Return a placeholder tool that reports the error
return [
tool({
name: "tw_stock_error",
description: "TW Stock MCP server is not available. Check logs for details.",
parameters: {
message: z.string().optional().describe("Your query (will return error message)"),
},
implementation: async () => {
return "❌ TWStock MCP Server is not running. " +
"Please ensure:\n" +
"1. Python is installed\n" +
"2. Run 'npm run setup-python' to install dependencies\n" +
"3. TWStockMCPServer/venv exists with Python installed";
},
}),
];
}
const mcpTools = mcp.getTools();
console.log(`[TW-Stock] Exposing ${mcpTools.length} MCP tools to LM Studio`);
return mcpTools.map((mcpTool: MCPTool) => {
const zodSchemaShape = buildZodSchema(mcpTool);
return tool({
name: mcpTool.name,
description: mcpTool.description || `MCP tool: ${mcpTool.name}`,
parameters: zodSchemaShape,
implementation: async (args: any) => {
console.log(`[TW-Stock] Calling tool: ${mcpTool.name}`);
try {
const result = await mcp.callTool(mcpTool.name, args);
if (result?.content) {
return result.content.map((c: any) => c.text || "").join("\n\n") || "(empty)";
}
return typeof result === "string" ? result : JSON.stringify(result);
} catch (error) {
const errMsg = (error as Error).message;
console.error(`[TW-Stock] Tool ${mcpTool.name} failed:`, errMsg);
return `Error: ${errMsg}`;
}
},
});
});
};