src / mcpClient.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import * as path from "path";
import * as fs from "fs";
export interface McpClientManager {
listTools(): Promise<any[]>;
callTool(name: string, args: Record<string, any>, signal?: AbortSignal): Promise<any>;
close(): Promise<void>;
}
function getPythonExecutable(): string {
const pluginRoot = path.resolve(__dirname, "..");
const maverickRoot = path.join(pluginRoot, "maverick-mcp");
const venvDir = path.join(maverickRoot, ".venv");
if (process.platform === "win32") {
return path.join(venvDir, "Scripts", "python.exe");
} else {
return path.join(venvDir, "bin", "python");
}
}
function getMaverickRoot(): string {
return path.join(path.resolve(__dirname, ".."), "maverick-mcp");
}
class McpClientManagerImpl implements McpClientManager {
private client: Client | null = null;
private transport: StdioClientTransport | null = null;
private initialized = false;
private initPromise: Promise<void> | null = null;
private async ensureInitialized(): Promise<void> {
if (this.initialized) return;
if (this.initPromise) return this.initPromise;
this.initPromise = (async () => {
const python = getPythonExecutable();
const maverickRoot = getMaverickRoot();
console.error(`[maverick-lms-mcp] Python path: ${python}`);
console.error(`[maverick-lms-mcp] Maverick root: ${maverickRoot}`);
// 確認 Python 存在
if (!fs.existsSync(python)) {
throw new Error(
`Python venv not found at ${python}. ` +
`Please ensure 'npm install' ran successfully.`
);
}
// 確認 maverick-mcp 目錄存在
if (!fs.existsSync(maverickRoot)) {
throw new Error(
`MaverickMCP directory not found at ${maverickRoot}. ` +
`Please run 'npm install' to clone the repository.`
);
}
console.error(`[maverick-lms-mcp] Starting Python MCP server...`);
this.transport = new StdioClientTransport({
command: python,
args: ["-m", "maverick.server", "--transport", "stdio"],
cwd: maverickRoot,
stderr: "pipe", // 重要:捕捉 stderr
});
// 監聽 stderr 輸出以診斷問題
this.transport.stderr?.on("data", (data: Buffer) => {
const message = data.toString();
console.error(`[maverick-mcp-stderr] ${message}`);
});
this.transport.stderr?.on("error", (err: Error) => {
console.error(`[maverick-mcp-stderr-error] ${err.message}`);
});
this.client = new Client(
{ name: "maverick-lms-mcp", version: "1.0.0" },
{ capabilities: {} }
);
try {
console.error(`[maverick-lms-mcp] Connecting to MCP server...`);
await this.client.connect(this.transport);
console.error(`[maverick-lms-mcp] Successfully connected to MCP server`);
this.initialized = true;
} catch (err: any) {
console.error(`[maverick-lms-mcp] Failed to connect: ${err.message}`);
console.error(`[maverick-lms-mcp] Stack: ${err.stack}`);
throw err;
}
})();
return this.initPromise;
}
async listTools(): Promise<any[]> {
await this.ensureInitialized();
if (!this.client) throw new Error("MCP client not initialized");
try {
console.error(`[maverick-lms-mcp] Listing tools...`);
const result = await this.client.listTools();
console.error(`[maverick-lms-mcp] Found ${result.tools?.length ?? 0} tools`);
return result.tools ?? [];
} catch (err: any) {
console.error(`[maverick-lms-mcp] Failed to list tools: ${err.message}`);
throw err;
}
}
async callTool(name: string, args: Record<string, any>, signal?: AbortSignal): Promise<any> {
await this.ensureInitialized();
if (!this.client) throw new Error("MCP client not initialized");
try {
console.error(`[maverick-lms-mcp] Calling tool: ${name}`);
const result = await this.client.callTool({ name, arguments: args });
return result;
} catch (err: any) {
console.error(`[maverick-lms-mcp] Failed to call tool ${name}: ${err.message}`);
throw err;
}
}
async close(): Promise<void> {
if (this.transport) {
try {
await this.transport.close();
} catch {
// ignore
}
}
this.client = null;
this.transport = null;
this.initialized = false;
}
}
let instance: McpClientManagerImpl | null = null;
export async function getMcpClient(): Promise<McpClientManager> {
if (!instance) {
instance = new McpClientManagerImpl();
}
return instance;
}src / mcpClient.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import * as path from "path";
import * as fs from "fs";
export interface McpClientManager {
listTools(): Promise<any[]>;
callTool(name: string, args: Record<string, any>, signal?: AbortSignal): Promise<any>;
close(): Promise<void>;
}
function getPythonExecutable(): string {
const pluginRoot = path.resolve(__dirname, "..");
const maverickRoot = path.join(pluginRoot, "maverick-mcp");
const venvDir = path.join(maverickRoot, ".venv");
if (process.platform === "win32") {
return path.join(venvDir, "Scripts", "python.exe");
} else {
return path.join(venvDir, "bin", "python");
}
}
function getMaverickRoot(): string {
return path.join(path.resolve(__dirname, ".."), "maverick-mcp");
}
class McpClientManagerImpl implements McpClientManager {
private client: Client | null = null;
private transport: StdioClientTransport | null = null;
private initialized = false;
private initPromise: Promise<void> | null = null;
private async ensureInitialized(): Promise<void> {
if (this.initialized) return;
if (this.initPromise) return this.initPromise;
this.initPromise = (async () => {
const python = getPythonExecutable();
const maverickRoot = getMaverickRoot();
console.error(`[maverick-lms-mcp] Python path: ${python}`);
console.error(`[maverick-lms-mcp] Maverick root: ${maverickRoot}`);
// 確認 Python 存在
if (!fs.existsSync(python)) {
throw new Error(
`Python venv not found at ${python}. ` +
`Please ensure 'npm install' ran successfully.`
);
}
// 確認 maverick-mcp 目錄存在
if (!fs.existsSync(maverickRoot)) {
throw new Error(
`MaverickMCP directory not found at ${maverickRoot}. ` +
`Please run 'npm install' to clone the repository.`
);
}
console.error(`[maverick-lms-mcp] Starting Python MCP server...`);
this.transport = new StdioClientTransport({
command: python,
args: ["-m", "maverick.server", "--transport", "stdio"],
cwd: maverickRoot,
stderr: "pipe", // 重要:捕捉 stderr
});
// 監聽 stderr 輸出以診斷問題
this.transport.stderr?.on("data", (data: Buffer) => {
const message = data.toString();
console.error(`[maverick-mcp-stderr] ${message}`);
});
this.transport.stderr?.on("error", (err: Error) => {
console.error(`[maverick-mcp-stderr-error] ${err.message}`);
});
this.client = new Client(
{ name: "maverick-lms-mcp", version: "1.0.0" },
{ capabilities: {} }
);
try {
console.error(`[maverick-lms-mcp] Connecting to MCP server...`);
await this.client.connect(this.transport);
console.error(`[maverick-lms-mcp] Successfully connected to MCP server`);
this.initialized = true;
} catch (err: any) {
console.error(`[maverick-lms-mcp] Failed to connect: ${err.message}`);
console.error(`[maverick-lms-mcp] Stack: ${err.stack}`);
throw err;
}
})();
return this.initPromise;
}
async listTools(): Promise<any[]> {
await this.ensureInitialized();
if (!this.client) throw new Error("MCP client not initialized");
try {
console.error(`[maverick-lms-mcp] Listing tools...`);
const result = await this.client.listTools();
console.error(`[maverick-lms-mcp] Found ${result.tools?.length ?? 0} tools`);
return result.tools ?? [];
} catch (err: any) {
console.error(`[maverick-lms-mcp] Failed to list tools: ${err.message}`);
throw err;
}
}
async callTool(name: string, args: Record<string, any>, signal?: AbortSignal): Promise<any> {
await this.ensureInitialized();
if (!this.client) throw new Error("MCP client not initialized");
try {
console.error(`[maverick-lms-mcp] Calling tool: ${name}`);
const result = await this.client.callTool({ name, arguments: args });
return result;
} catch (err: any) {
console.error(`[maverick-lms-mcp] Failed to call tool ${name}: ${err.message}`);
throw err;
}
}
async close(): Promise<void> {
if (this.transport) {
try {
await this.transport.close();
} catch {
// ignore
}
}
this.client = null;
this.transport = null;
this.initialized = false;
}
}
let instance: McpClientManagerImpl | null = null;
export async function getMcpClient(): Promise<McpClientManager> {
if (!instance) {
instance = new McpClientManagerImpl();
}
return instance;
}