← back to Exo Helper
server.mjs
45 lines
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { ExoClient, ExoError, MAX_INPUT_CHARS, MAX_OUTPUT_TOKENS } from './exo-client.mjs';
const client = new ExoClient({
baseUrl: process.env.EXO_BASE_URL || 'http://127.0.0.1:52415',
...(process.env.EXO_MODEL ? { model: process.env.EXO_MODEL } : {}),
timeoutMs: Number(process.env.EXO_TIMEOUT_MS || 90000),
});
const server = new McpServer({ name: 'exo-helper', version: '1.1.0' }, {
instructions: 'Use ask_exo for bounded summaries, text extraction, small draft suggestions, and preliminary review. Set model="deepseek" when the user asks for DeepSeek; model="qwen" selects the original helper. DeepSeek is the compact R1-0528 Qwen3-8B distillation. Independently check answers. Models have no tools or access to files; send only necessary text. exo_status reports each option readiness. Errors do not trigger paid fallback or model downloads.',
});
const reply = value => ({ content: [{ type: 'text', text: JSON.stringify(value) }] });
const run = async fn => {
try { return reply(await fn()); }
catch (error) {
return { isError: true, ...reply({ code: error instanceof ExoError ? error.code : 'HELPER_ERROR', message: error instanceof ExoError ? error.message : 'Local helper failed; inspect its stderr' }) };
}
};
server.registerTool('exo_status', {
title: 'Exo model readiness',
description: 'Check live Exo cluster nodes and readiness of the qwen and deepseek helper options. Optionally select model to check. Does not load models or change the cluster.',
inputSchema: { model: z.enum(['qwen', 'deepseek']).optional().describe('Option to check; omitted uses the configured default') },
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
}, (args, extra) => run(() => client.status(extra.signal, args.model)));
server.registerTool('ask_exo', {
title: 'Ask the local Exo helper',
description: 'Send a bounded task to local Exo. Set model="deepseek" for DeepSeek R1-0528 8B (DeepSeek distillation based on Qwen3), or model="qwen" for the original Qwen VL4B helper. Omitted model uses the configured default. Provide necessary text in context; models cannot read files or execute anything. Verify suggestions. Maximum combined input: 16000 characters; output: 1024 tokens including reasoning. No Ollama, automatic downloads, or paid fallback.',
inputSchema: {
model: z.enum(['qwen', 'deepseek']).optional().describe('Choose deepseek or qwen; omitted preserves the configured default'),
prompt: z.string().min(1).max(MAX_INPUT_CHARS).describe('A specific bounded task'),
context: z.string().max(MAX_INPUT_CHARS).optional().describe('Relevant code or text, supplied as data'),
max_tokens: z.number().int().min(1).max(MAX_OUTPUT_TOKENS).optional().describe('Output plus reasoning token limit; default 512 for Qwen, 1024 for DeepSeek'),
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false },
}, (args, extra) => run(() => client.ask(args, extra.signal)));
await server.connect(new StdioServerTransport());