← back to Kimi Mcp
server.mjs
126 lines
#!/usr/bin/env node
// kimi-mcp — owned MCP server exposing Moonshot Kimi as consult tools.
// Scope: consult only (ask / review). Kimi gets NO filesystem, bash, or agent
// loop here — it answers, it does not act. Model + temperature match the /dtd
// panel (kimi-k2.5, temp=1) so this is the same Kimi lens Steve already trusts.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
// ── Constants ────────────────────────────────────────────────────────────────
const MOONSHOT_API_KEY = process.env.MOONSHOT_API_KEY;
const MOONSHOT_BASE = 'https://api.moonshot.ai/v1'; // global endpoint (matches /dtd), NOT .cn
const MODEL = process.env.KIMI_MODEL || 'kimi-k2.5'; // /dtd bakeoff pick; override via KIMI_MODEL
const TEMPERATURE = 1; // Moonshot REQUIRES temp=1 for the k2.x/k3 family
const MAX_TOKENS = Number(process.env.KIMI_MAX_TOKENS || 8000);
// kimi-k2.5 pay-as-you-go rates (per token), from cost-tracker/pricing.json
const RATE_IN = 5.5e-7;
const RATE_OUT = 2.2e-6;
// ── Moonshot chat helper ───────────────────────────────────────────────────
// Returns { text, usage, cost }. Handles the reasoning-model quirk where the
// visible answer lands in reasoning_content while content comes back empty
// (the exact failure that produced silent-empty Kimi votes in /dtd).
async function askKimi(messages) {
if (!MOONSHOT_API_KEY) throw new Error('MOONSHOT_API_KEY is not set');
const body = {
model: MODEL,
messages,
temperature: TEMPERATURE,
max_tokens: MAX_TOKENS,
};
const call = () => fetch(`${MOONSHOT_BASE}/chat/completions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${MOONSHOT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
let res = await call();
if (res.status === 429) { // gentle single retry on rate-limit
await new Promise(r => setTimeout(r, 10_000));
res = await call();
}
if (res.status === 401) throw new Error('MOONSHOT_API_KEY invalid or expired');
if (!res.ok) throw new Error(`Moonshot API error ${res.status}: ${await res.text()}`);
const data = await res.json();
const msg = data.choices?.[0]?.message || {};
const text = (msg.content && msg.content.trim())
|| (msg.reasoning_content && msg.reasoning_content.trim()) // reasoning-model fallback
|| '[kimi returned an empty message]';
const usage = data.usage || { prompt_tokens: 0, completion_tokens: 0 };
const cost = usage.prompt_tokens * RATE_IN + usage.completion_tokens * RATE_OUT;
return { text, usage, cost };
}
// One-line cost footer so every paid call surfaces its spend (Steve's rule).
function footer({ usage, cost }) {
return `\n\n---\n_Kimi ${MODEL} · ${usage.prompt_tokens || 0} in / ${usage.completion_tokens || 0} out · ~$${cost.toFixed(4)}_`;
}
function ok(result) {
return { content: [{ type: 'text', text: result.text + footer(result) }] };
}
function fail(err) {
return { content: [{ type: 'text', text: `Kimi error: ${err.message}` }], isError: true };
}
// ── MCP server ───────────────────────────────────────────────────────────────
const server = new McpServer({ name: 'kimi-mcp', version: '1.0.0' });
server.tool(
'ask_kimi',
'Ask Moonshot Kimi (kimi-k2.5) a question and return its answer. Use for a second-opinion / independent reasoning lens, or to offload a self-contained analysis to a different model. Kimi has no access to your files — include any needed context in the prompt.',
{
prompt: z.string().describe('The question or task for Kimi. Self-contained — paste any needed context/code inline.'),
system: z.string().optional().describe('Optional system instruction to steer Kimi (persona, format, constraints).'),
},
async ({ prompt, system }) => {
try {
const messages = [];
if (system) messages.push({ role: 'system', content: system });
messages.push({ role: 'user', content: prompt });
return ok(await askKimi(messages));
} catch (err) {
return fail(err);
}
},
);
server.tool(
'review_with_kimi',
'Have Kimi (kimi-k2.5) review a code snippet or diff for bugs, security issues, edge cases, and quality — an independent adversarial reviewer lens. Paste the code directly (Kimi cannot read your filesystem).',
{
code: z.string().describe('The code, snippet, or unified diff to review. Paste it inline.'),
focus: z.string().optional().describe('Optional aspect to prioritize (e.g. "security", "concurrency", "the error paths").'),
context: z.string().optional().describe('Optional surrounding context — what the code does, language, constraints.'),
},
async ({ code, focus, context }) => {
try {
const focusNote = focus ? ` Focus especially on: ${focus}.` : '';
const ctxNote = context ? `Context: ${context}\n\n` : '';
const messages = [
{
role: 'system',
content: `You are an expert, skeptical code reviewer. Find real bugs, security issues, race conditions, edge cases, and quality problems. Be specific and actionable — cite the line/construct and give the fix. If the code is genuinely fine, say so briefly rather than inventing nits.${focusNote}`,
},
{ role: 'user', content: `${ctxNote}Review this code:\n\n\`\`\`\n${code}\n\`\`\`` },
];
return ok(await askKimi(messages));
} catch (err) {
return fail(err);
}
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`kimi-mcp up · model=${MODEL} · key=${MOONSHOT_API_KEY ? 'present' : 'MISSING'}`);