← 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());