src / toolsProvider.ts
import { tool, Tool, ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { configSchematics } from "./config";
import { getMcpClient } from "./mcpBridge";
function jsonSchemaToZod(schema: any): z.ZodTypeAny {
if (!schema) return z.any();
if (schema.type === "string") {
let zStr = z.string();
if (schema.description) zStr = zStr.describe(schema.description);
return zStr;
} else if (schema.type === "number" || schema.type === "integer") {
let zNum = z.number();
if (schema.type === "integer") zNum = zNum.int();
if (schema.minimum !== undefined) zNum = zNum.min(schema.minimum);
if (schema.maximum !== undefined) zNum = zNum.max(schema.maximum);
if (schema.description) zNum = zNum.describe(schema.description);
return zNum;
} else if (schema.type === "boolean") {
let zBool = z.boolean();
if (schema.description) zBool = zBool.describe(schema.description);
return zBool;
} else if (schema.type === "array") {
let zArr = z.array(jsonSchemaToZod(schema.items));
if (schema.description) zArr = zArr.describe(schema.description);
return zArr;
} else if (schema.type === "object") {
let shape: Record<string, z.ZodTypeAny> = {};
if (schema.properties) {
for (const [key, prop] of Object.entries<any>(schema.properties)) {
let propZod = jsonSchemaToZod(prop);
if (!(schema.required || []).includes(key)) {
propZod = propZod.optional();
}
shape[key] = propZod;
}
}
let zObj = z.object(shape).passthrough();
if (schema.description) zObj = zObj.describe(schema.description);
return zObj;
} else if (schema.anyOf) {
return z.any().describe(schema.description || "");
}
return z.any().describe(schema.description || "");
}
function convertMcpParamsToZod(inputSchema: any): Record<string, any> {
if (!inputSchema || !inputSchema.properties) return {};
const zodParams: Record<string, any> = {};
for (const [key, prop] of Object.entries<any>(inputSchema.properties)) {
let zProp = jsonSchemaToZod(prop);
if (!(inputSchema.required || []).includes(key)) {
zProp = zProp.optional();
}
zodParams[key] = zProp;
}
return zodParams;
}
let cachedTools: Tool[] | null = null;
let lastApiKey: string | null = null;
export async function toolsProvider(ctl: ToolsProviderController): Promise<Tool[]> {
const apiKey = ctl.getPluginConfig(configSchematics).get("marketauxApiKey") || "";
// 使用快取避免每次對話重複初始化
if (cachedTools && lastApiKey === apiKey) {
return cachedTools;
}
lastApiKey = apiKey;
try {
// 使用 console.log 輸出初始化狀態到 LM Studio 的 Plugin Console
console.log("[TradingView MCP] Connecting to TradingView MCP server...");
const client = await getMcpClient(apiKey, (msg) => console.log(`[TradingView MCP] ${msg}`));
console.log("[TradingView MCP] Fetching tools from TradingView MCP server...");
const mcpTools = await client.listTools();
const tools: Tool[] = [];
for (const mcpTool of mcpTools.tools) {
const zodParams = convertMcpParamsToZod(mcpTool.inputSchema);
const lmStudioTool = tool({
name: mcpTool.name,
description: mcpTool.description || "",
parameters: zodParams,
// 注意:status 和 warn 是從這裡的 context 解構出來的
implementation: async (args: any, { status, warn }: any) => {
try {
status(`Executing ${mcpTool.name}...`);
const result = await client.callTool({
name: mcpTool.name,
arguments: args
});
if (result.isError) {
warn(`Tool ${mcpTool.name} returned an error`);
}
return result.content.map((c: any) => c.text || JSON.stringify(c)).join("\n");
} catch (err: any) {
warn(`Error calling tool: ${err.message}`);
return `Error: ${err.message}`;
}
}
});
tools.push(lmStudioTool);
}
cachedTools = tools;
return tools;
} catch (err: any) {
// 使用 console.warn 輸出錯誤
console.warn(`[TradingView MCP] Failed to initialize: ${err.message}`);
return [
tool({
name: "tradingview_error",
description: "Failed to initialize TradingView MCP server. Check plugin settings and console logs.",
parameters: {},
implementation: async () => `Error: ${err.message}`
})
];
}
}src / toolsProvider.ts
import { tool, Tool, ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { configSchematics } from "./config";
import { getMcpClient } from "./mcpBridge";
function jsonSchemaToZod(schema: any): z.ZodTypeAny {
if (!schema) return z.any();
if (schema.type === "string") {
let zStr = z.string();
if (schema.description) zStr = zStr.describe(schema.description);
return zStr;
} else if (schema.type === "number" || schema.type === "integer") {
let zNum = z.number();
if (schema.type === "integer") zNum = zNum.int();
if (schema.minimum !== undefined) zNum = zNum.min(schema.minimum);
if (schema.maximum !== undefined) zNum = zNum.max(schema.maximum);
if (schema.description) zNum = zNum.describe(schema.description);
return zNum;
} else if (schema.type === "boolean") {
let zBool = z.boolean();
if (schema.description) zBool = zBool.describe(schema.description);
return zBool;
} else if (schema.type === "array") {
let zArr = z.array(jsonSchemaToZod(schema.items));
if (schema.description) zArr = zArr.describe(schema.description);
return zArr;
} else if (schema.type === "object") {
let shape: Record<string, z.ZodTypeAny> = {};
if (schema.properties) {
for (const [key, prop] of Object.entries<any>(schema.properties)) {
let propZod = jsonSchemaToZod(prop);
if (!(schema.required || []).includes(key)) {
propZod = propZod.optional();
}
shape[key] = propZod;
}
}
let zObj = z.object(shape).passthrough();
if (schema.description) zObj = zObj.describe(schema.description);
return zObj;
} else if (schema.anyOf) {
return z.any().describe(schema.description || "");
}
return z.any().describe(schema.description || "");
}
function convertMcpParamsToZod(inputSchema: any): Record<string, any> {
if (!inputSchema || !inputSchema.properties) return {};
const zodParams: Record<string, any> = {};
for (const [key, prop] of Object.entries<any>(inputSchema.properties)) {
let zProp = jsonSchemaToZod(prop);
if (!(inputSchema.required || []).includes(key)) {
zProp = zProp.optional();
}
zodParams[key] = zProp;
}
return zodParams;
}
let cachedTools: Tool[] | null = null;
let lastApiKey: string | null = null;
export async function toolsProvider(ctl: ToolsProviderController): Promise<Tool[]> {
const apiKey = ctl.getPluginConfig(configSchematics).get("marketauxApiKey") || "";
// 使用快取避免每次對話重複初始化
if (cachedTools && lastApiKey === apiKey) {
return cachedTools;
}
lastApiKey = apiKey;
try {
// 使用 console.log 輸出初始化狀態到 LM Studio 的 Plugin Console
console.log("[TradingView MCP] Connecting to TradingView MCP server...");
const client = await getMcpClient(apiKey, (msg) => console.log(`[TradingView MCP] ${msg}`));
console.log("[TradingView MCP] Fetching tools from TradingView MCP server...");
const mcpTools = await client.listTools();
const tools: Tool[] = [];
for (const mcpTool of mcpTools.tools) {
const zodParams = convertMcpParamsToZod(mcpTool.inputSchema);
const lmStudioTool = tool({
name: mcpTool.name,
description: mcpTool.description || "",
parameters: zodParams,
// 注意:status 和 warn 是從這裡的 context 解構出來的
implementation: async (args: any, { status, warn }: any) => {
try {
status(`Executing ${mcpTool.name}...`);
const result = await client.callTool({
name: mcpTool.name,
arguments: args
});
if (result.isError) {
warn(`Tool ${mcpTool.name} returned an error`);
}
return result.content.map((c: any) => c.text || JSON.stringify(c)).join("\n");
} catch (err: any) {
warn(`Error calling tool: ${err.message}`);
return `Error: ${err.message}`;
}
}
});
tools.push(lmStudioTool);
}
cachedTools = tools;
return tools;
} catch (err: any) {
// 使用 console.warn 輸出錯誤
console.warn(`[TradingView MCP] Failed to initialize: ${err.message}`);
return [
tool({
name: "tradingview_error",
description: "Failed to initialize TradingView MCP server. Check plugin settings and console logs.",
parameters: {},
implementation: async () => `Error: ${err.message}`
})
];
}
}