// Cloudflare Worker entrypoint for Vibe Check MCP (JSON-RPC)
// - Implements fetch handler (no Express, no Node http)
// - Supports methods: initialize, tools/list, tools/call (vibe_check, vibe_learn)
// - Worker-safe LLM invocation (no fs, no Node-only modules)
// - Persists vibe_learn patterns in KV (binding: VIBE_LEARN)
interface Env {
GEMINI_API_KEY?: string;
OPENAI_API_KEY?: string;
OPENROUTER_API_KEY?: string;
DEFAULT_LLM_PROVIDER?: string;
DEFAULT_MODEL?: string;
VIBE_LEARN?: any; // KVNamespace for vibe_learn
VIBE_CONSTITUTION?: any; // KVNamespace for constitution
CORS_ORIGIN?: string; // Optional CORS allow origin
}
type JSONValue = string | number | boolean | null | JSONObject | JSONArray;
interface JSONObject { [key: string]: JSONValue }
interface JSONArray extends Array<JSONValue> {}
type JSONRPCId = string | number | null;
interface JSONRPCRequest {
jsonrpc: '2.0';
id?: JSONRPCId;
method: string;
params?: any;
}
interface JSONRPCResponse {
jsonrpc: '2.0';
id: JSONRPCId;
result?: any;
error?: { code: number; message: string; data?: any };
}
const SERVER_INFO = { name: 'vibe-check', version: '2.5.1' };
const PROTOCOL_VERSION = '2025-03-26';
let CURRENT_SESSION_ID: string | undefined;
function toolsListPayload() {
return {
tools: [
{
name: 'vibe_check',
description:
'Metacognitive questioning tool that identifies assumptions and breaks tunnel vision to prevent cascading errors',
inputSchema: {
type: 'object',
properties: {
goal: { type: 'string', description: "The agent's current goal" },
plan: { type: 'string', description: "The agent's detailed plan" },
modelOverride: {
type: 'object',
properties: {
provider: { type: 'string', enum: ['gemini', 'openai', 'openrouter'] },
model: { type: 'string' },
},
required: [],
},
userPrompt: { type: 'string', description: 'The original user prompt' },
progress: { type: 'string', description: "The agent's progress so far" },
uncertainties: { type: 'array', items: { type: 'string' }, description: "The agent's uncertainties" },
taskContext: { type: 'string', description: 'The context of the current task' },
sessionId: { type: 'string', description: 'Optional session ID for state management' },
},
required: ['goal', 'plan'],
additionalProperties: false,
},
},
{
name: 'vibe_learn',
description: 'Pattern recognition system that tracks common errors and solutions to prevent recurring issues',
inputSchema: {
type: 'object',
properties: {
mistake: { type: 'string', description: 'One-sentence description of the learning entry' },
category: { type: 'string', description: 'Category for the entry' },
solution: { type: 'string', description: 'How it was corrected (if applicable)' },
type: { type: 'string', enum: ['mistake', 'preference', 'success'], description: 'Type of learning entry' },
sessionId: { type: 'string', description: 'Optional session ID for state management' },
},
required: ['mistake', 'category'],
additionalProperties: false,
},
},
{
name: 'update_constitution',
description: 'Append a constitutional rule for this session (in-memory)',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string' },
rule: { type: 'string' },
},
required: ['sessionId', 'rule'],
additionalProperties: false,
},
},
{
name: 'reset_constitution',
description: 'Overwrite all constitutional rules for this session',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string' },
rules: { type: 'array', items: { type: 'string' } },
},
required: ['sessionId', 'rules'],
additionalProperties: false,
},
},
{
name: 'check_constitution',
description: 'Return the current constitution rules for this session',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string' },
},
required: ['sessionId'],
additionalProperties: false,
},
},
],
};
}
function corsHeaders(env?: Env): Headers {
const h = new Headers();
h.set('Access-Control-Allow-Origin', env?.CORS_ORIGIN || '*');
h.set('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
h.set('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id, Mcp-Protocol-Version');
h.set('Access-Control-Expose-Headers', 'mcp-protocol-version, mcp-session-id');
h.set('mcp-protocol-version', PROTOCOL_VERSION);
if (CURRENT_SESSION_ID) h.set('mcp-session-id', CURRENT_SESSION_ID);
return h;
}
function jsonResponse(data: any, init: ResponseInit = {}, env?: Env) {
const headers = new Headers(init.headers);
if (!headers.has('content-type')) headers.set('content-type', 'application/json');
const ch = corsHeaders(env);
for (const [k, v] of ch.entries()) headers.set(k, v);
return new Response(JSON.stringify(data), { ...init, headers });
}
function error(id: JSONRPCId, code: number, message: string, data?: any): JSONRPCResponse {
return { jsonrpc: '2.0', id: id ?? null, error: { code, message, data } };
}
function ok(id: JSONRPCId, result: any): JSONRPCResponse {
return { jsonrpc: '2.0', id: id ?? null, result };
}
async function handleRpc(request: JSONRPCRequest, env: Env): Promise<JSONRPCResponse> {
const id = request.id ?? null;
try {
switch (request.method) {
case 'initialize': {
try { CURRENT_SESSION_ID = (globalThis as any).crypto?.randomUUID?.() || CURRENT_SESSION_ID; } catch {}
return ok(id, {
protocolVersion: PROTOCOL_VERSION,
serverInfo: SERVER_INFO,
capabilities: { tools: {}, sampling: {} },
});
}
case 'tools/list': {
return ok(id, toolsListPayload());
}
case 'tools/call': {
const { name, arguments: args } = request.params ?? {};
if (name === 'vibe_check') {
const input = {
goal: args?.goal,
plan: args?.plan,
modelOverride: args?.modelOverride,
userPrompt: args?.userPrompt,
progress: args?.progress,
uncertainties: args?.uncertainties,
taskContext: args?.taskContext,
sessionId: args?.sessionId,
};
if (!input.goal || !input.plan) {
return error(id, -32602, 'Missing required params: goal, plan');
}
const result = await getMetacognitiveQuestionsWorker(env, input);
return ok(id, { content: [{ type: 'text', text: result.questions }] });
} else if (name === 'vibe_learn') {
if (!env.VIBE_LEARN) {
return error(id, -32000, 'VIBE_LEARN KV binding is not configured.');
}
const resp = await handleVibeLearnKV(env, args || {});
return ok(id, { content: [{ type: 'text', text: formatVibeLearnOutput(resp) }] });
} else if (name === 'update_constitution') {
const sessionId = String(args?.sessionId || '');
const rule = String(args?.rule || '');
if (!sessionId || !rule) return error(id, -32602, 'Missing: sessionId, rule');
if (env.VIBE_CONSTITUTION) {
await updateConstitutionKV(env, sessionId, rule);
} else {
updateConstitutionWorker(sessionId, rule);
}
return ok(id, { content: [{ type: 'text', text: '✅ Constitution updated' }] });
} else if (name === 'reset_constitution') {
const sessionId = String(args?.sessionId || '');
const rules = Array.isArray(args?.rules) ? args.rules.map(String) : null;
if (!sessionId || !rules) return error(id, -32602, 'Missing: sessionId, rules');
if (env.VIBE_CONSTITUTION) {
await resetConstitutionKV(env, sessionId, rules);
} else {
resetConstitutionWorker(sessionId, rules);
}
return ok(id, { content: [{ type: 'text', text: '✅ Constitution reset' }] });
} else if (name === 'check_constitution') {
const sessionId = String(args?.sessionId || '');
if (!sessionId) return error(id, -32602, 'Missing: sessionId');
const rules = env.VIBE_CONSTITUTION
? await getConstitutionKV(env, sessionId)
: getConstitutionWorker(sessionId);
return ok(id, { content: [{ type: 'text', text: JSON.stringify({ rules }) }] });
} else {
return error(id, -32601, `Unknown tool: ${name}`);
}
}
default:
return error(id, -32601, `Method not found: ${request.method}`);
}
} catch (e: any) {
return error(id, -32603, 'Internal error', String(e?.message || e));
}
}
export default {
async fetch(req: Request, env: Env): Promise<Response> {
const url = new URL(req.url);
if (req.method === 'GET' && url.pathname === '/healthz') {
return jsonResponse({ status: 'ok' }, {}, env);
}
// Minimal SSE support for Streamable HTTP clients
if (url.pathname === '/mcp' && req.method === 'GET') {
const accept = req.headers.get('accept') || '';
if (!accept.includes('text/event-stream')) {
return jsonResponse({ jsonrpc: '2.0', id: null, error: { code: -32000, message: 'Not Acceptable: Client must accept text/event-stream' } }, { status: 406 }, env);
}
return openSSE(env);
}
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders(env) });
}
if (url.pathname === '/mcp' && req.method === 'POST') {
let body: any;
try {
body = await req.json();
} catch (e) {
return jsonResponse({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }, { status: 400 }, env);
}
// Support single or batch JSON-RPC requests
if (Array.isArray(body)) {
const responses: JSONRPCResponse[] = [];
for (const msg of body) {
if (!msg || msg.jsonrpc !== '2.0' || typeof msg.method !== 'string') {
responses.push(error(msg?.id ?? null, -32600, 'Invalid Request'));
} else {
const r = await handleRpc(msg, env);
responses.push(r);
// Also stream to SSE if connected
sendSSE(r);
}
}
return jsonResponse(responses, {}, env);
} else {
if (!body || body.jsonrpc !== '2.0' || typeof body.method !== 'string') {
return jsonResponse(error(body?.id ?? null, -32600, 'Invalid Request'), {}, env);
}
const response = await handleRpc(body, env);
// Stream to SSE if available
sendSSE(response);
return jsonResponse(response, {}, env);
}
}
return new Response('Not Found', { status: 404 });
},
};
// ---------------- Worker-safe LLM ----------------
type WCModelOverride = { provider?: string; model?: string };
type VibeCheckInputLite = {
goal: string;
plan: string;
modelOverride?: WCModelOverride;
userPrompt?: string;
progress?: string;
uncertainties?: string[];
taskContext?: string;
sessionId?: string;
};
async function getMetacognitiveQuestionsWorker(env: Env, input: VibeCheckInputLite): Promise<{ questions: string }> {
const provider = input.modelOverride?.provider || env.DEFAULT_LLM_PROVIDER || 'gemini';
const model = input.modelOverride?.model || env.DEFAULT_MODEL;
const systemPrompt = `You are a meta-mentor. You're an experienced feedback provider that specializes in understanding intent, dysfunctional patterns in AI agents, and in responding in ways that further the goal. You need to carefully reason and process the information provided, to determine your output.\n\nYour tone needs to always be a mix of these traits based on the context of which pushes the message in the most appropriate affect: Gentle & Validating, Unafraid to push many questions but humble enough to step back, Sharp about problems and eager to help about problem-solving & giving tips and/or advice, stern and straightforward when spotting patterns & the agent being stuck in something that could derail things.\n\nHere's what you need to think about (Do not output the full thought process, only what is explicitly requested):\n1. What's going on here? What's the nature of the problem is the agent tackling? What's the approach, situation and goal? Is there any prior context that clarifies context further? \n2. What does the agent need to hear right now: Are there any clear patterns, loops, or unspoken assumptions being missed here? Or is the agent doing fine - in which case should I interrupt it or provide soft encouragement and a few questions? What is the best response I can give right now?\n3. In case the issue is technical - I need to provide guidance and help. In case I spot something that's clearly not accounted for/ assumed/ looping/ or otherwise could be out of alignment with the user or agent stated goals - I need to point out what I see gently and ask questions on if the agent agrees. If I don't see/ can't interpret an explicit issue - what intervention would provide valuable feedback here - questions, guidance, validation, or giving a soft go-ahead with reminders of best practices?\n4. In case the plan looks to be accurate - based on the context, can I remind the agent of how to continue, what not to forget, or should I soften and step back for the agent to continue its work? What's the most helpful thing I can do right now?`;
const contextSection = `CONTEXT:\nGoal: ${input.goal}\nPlan: ${input.plan}\nProgress: ${input.progress || 'None'}\nUncertainties: ${input.uncertainties?.join(', ') || 'None'}\nTask Context: ${input.taskContext || 'None'}\nUser Prompt: ${input.userPrompt || 'None'}`;
const fullPrompt = `${systemPrompt}\n\n${contextSection}`;
try {
if (provider === 'gemini') {
if (!env.GEMINI_API_KEY) throw new Error('Gemini API key missing.');
const { GoogleGenerativeAI } = await import('@google/generative-ai');
const genAI = new GoogleGenerativeAI(env.GEMINI_API_KEY);
const geminiModel = model || 'gemini-2.5-pro';
try {
const modelInstance = genAI.getGenerativeModel({ model: geminiModel });
const result = await modelInstance.generateContent(fullPrompt);
return { questions: result.response.text() };
} catch (e) {
const fallbackModel = 'gemini-2.5-flash';
const modelInstance = genAI.getGenerativeModel({ model: fallbackModel });
const result = await modelInstance.generateContent(fullPrompt);
return { questions: result.response.text() };
}
} else if (provider === 'openai') {
if (!env.OPENAI_API_KEY) throw new Error('OpenAI API key missing.');
const { OpenAI } = await import('openai');
const client = new OpenAI({ apiKey: env.OPENAI_API_KEY });
const openaiModel = model || 'o4-mini';
const response = await client.chat.completions.create({
model: openaiModel,
messages: [{ role: 'system', content: fullPrompt }],
});
return { questions: response.choices?.[0]?.message?.content || '' };
} else if (provider === 'openrouter') {
if (!env.OPENROUTER_API_KEY) throw new Error('OpenRouter API key missing.');
if (!model) throw new Error('OpenRouter provider requires a model in modelOverride or DEFAULT_MODEL.');
const resp = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${env.OPENROUTER_API_KEY}`,
'HTTP-Referer': 'https://example.com',
'X-Title': 'Vibe Check MCP Worker',
},
body: JSON.stringify({ model, messages: [{ role: 'system', content: fullPrompt }] }),
});
if (!resp.ok) throw new Error(`OpenRouter error: ${resp.status}`);
const data: any = await resp.json();
return { questions: data?.choices?.[0]?.message?.content || '' };
} else {
throw new Error(`Invalid provider: ${provider}`);
}
} catch (error) {
return {
questions: `\nI can see you're thinking through your approach, which shows thoughtfulness:\n\n1. Does this plan directly address what the user requested, or might it be solving a different problem?\n2. Is there a simpler approach that would meet the user's needs?\n3. What unstated assumptions might be limiting the thinking here?\n4. How does this align with the user's original intent?\n`,
};
}
}
// ---------------- KV-backed vibe_learn ----------------
type LearningType = 'mistake' | 'preference' | 'success';
type LearningEntry = {
type: LearningType;
category: string;
mistake: string;
solution?: string;
timestamp: number;
};
type VibeLog = {
mistakes: Record<string, { count: number; examples: LearningEntry[]; lastUpdated: number }>;
lastUpdated: number;
};
const KV_LOG_KEY = 'vibe_learn:log';
async function kvReadLog(env: Env): Promise<VibeLog> {
try {
const data = await env.VIBE_LEARN.get(KV_LOG_KEY, 'json');
if (data && typeof data === 'object') return data as VibeLog;
} catch {}
return { mistakes: {}, lastUpdated: Date.now() };
}
async function kvWriteLog(env: Env, log: VibeLog): Promise<void> {
await env.VIBE_LEARN.put(KV_LOG_KEY, JSON.stringify(log));
}
function enforceOneSentence(text: string): string {
let sentence = text.replace(/\r?\n/g, ' ');
const parts = sentence.split(/([.!?])\s+/);
if (parts.length > 0) sentence = parts[0] + (parts[1] || '');
if (!/[.!?]$/.test(sentence)) sentence += '.';
return sentence.trim();
}
function isSimilar(a: string, b: string): boolean {
const aWords = a.toLowerCase().split(/\W+/).filter(Boolean);
const bWords = b.toLowerCase().split(/\W+/).filter(Boolean);
if (!aWords.length || !bWords.length) return false;
const overlap = aWords.filter(w => bWords.includes(w));
const ratio = overlap.length / Math.min(aWords.length, bWords.length);
return ratio >= 0.6;
}
function normalizeCategory(category: string): string {
const standard: Record<string, string[]> = {
'Complex Solution Bias': ['complex', 'complicated', 'over-engineered', 'complexity'],
'Feature Creep': ['feature', 'extra', 'additional', 'scope creep'],
'Premature Implementation': ['premature', 'early', 'jumping', 'too quick'],
'Misalignment': ['misaligned', 'wrong direction', 'off target', 'misunderstood'],
'Overtooling': ['overtool', 'too many tools', 'unnecessary tools'],
};
const lower = category.toLowerCase();
for (const [std, keywords] of Object.entries(standard)) {
if (keywords.some(k => lower.includes(k))) return std;
}
return category;
}
async function handleVibeLearnKV(env: Env, input: any) {
if (!input?.mistake) throw new Error('Mistake description is required');
if (!input?.category) throw new Error('Mistake category is required');
const type: LearningType = input.type ?? 'mistake';
if (type !== 'preference' && !input.solution) throw new Error('Solution is required for this entry type');
const mistake = enforceOneSentence(String(input.mistake));
const solution = input.solution ? enforceOneSentence(String(input.solution)) : undefined;
const category = normalizeCategory(String(input.category));
const log = await kvReadLog(env);
const existing = log.mistakes[category]?.examples || [];
const alreadyKnown = existing.some(e => isSimilar(e.mistake, mistake));
if (!alreadyKnown) {
const now = Date.now();
if (!log.mistakes[category]) {
log.mistakes[category] = { count: 0, examples: [], lastUpdated: now };
}
log.mistakes[category].count += 1;
log.mistakes[category].examples.push({ type, category, mistake, solution, timestamp: now });
log.mistakes[category].lastUpdated = now;
log.lastUpdated = now;
await kvWriteLog(env, log);
}
const summary = Object.entries(log.mistakes).map(([cat, data]) => ({
category: cat,
count: data.count,
recentExample: data.examples[data.examples.length - 1],
})).sort((a, b) => b.count - a.count);
const currentTally = log.mistakes[category]?.count || 1;
const topCategories = summary.slice(0, 3);
return { added: !alreadyKnown, alreadyKnown, currentTally, topCategories };
}
function formatVibeLearnOutput(result: { added: boolean; currentTally: number; alreadyKnown?: boolean; topCategories: Array<{ category: string; count: number; recentExample: LearningEntry }>; }): string {
let output = '';
if (result.added) output += `✅ Pattern logged successfully (category tally: ${result.currentTally})`;
else if (result.alreadyKnown) output += 'ℹ️ Pattern already recorded';
else output += '❌ Failed to log pattern';
if (result.topCategories?.length) {
output += '\n\n## Top Pattern Categories\n';
for (const c of result.topCategories) {
output += `\n### ${c.category} (${c.count} occurrences)\n`;
if (c.recentExample) {
output += `Most recent: "${c.recentExample.mistake}"\n`;
if (c.recentExample.solution) output += `Solution: "${c.recentExample.solution}"\n`;
}
}
}
return output;
}
// ---------------- SSE helpers ----------------
let sseController: ReadableStreamDefaultController<Uint8Array> | null = null;
const encoder = new TextEncoder();
function openSSE(env?: Env): Response {
const stream = new ReadableStream<Uint8Array>({
start(controller) {
sseController = controller;
// Send a comment to establish stream
controller.enqueue(encoder.encode(': connected\n\n'));
},
cancel() {
sseController = null;
},
});
const headers = new Headers({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
});
const ch = corsHeaders(env);
for (const [k, v] of ch.entries()) headers.set(k, v);
return new Response(stream, { headers });
}
function sendSSE(message: any) {
if (!sseController) return;
const payload = `event: message\ndata: ${JSON.stringify(message)}\n\n`;
try {
sseController.enqueue(encoder.encode(payload));
} catch {
sseController = null;
}
}
// ---------------- In-memory constitution (Worker) ----------------
type ConstitutionEntry = { rules: string[]; updated: number };
const constitutionMap: Record<string, ConstitutionEntry> = Object.create(null);
const MAX_RULES_PER_SESSION = 50;
const SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour
const SESSION_TTL_SEC = 60 * 60; // 1 hour for KV expiry
function cleanupConstitution() {
const now = Date.now();
for (const [sid, entry] of Object.entries(constitutionMap)) {
if (now - entry.updated > SESSION_TTL_MS) delete constitutionMap[sid];
}
}
function updateConstitutionWorker(sessionId: string, rule: string) {
cleanupConstitution();
const entry = constitutionMap[sessionId] || { rules: [], updated: 0 };
if (entry.rules.length >= MAX_RULES_PER_SESSION) entry.rules.shift();
entry.rules.push(rule);
entry.updated = Date.now();
constitutionMap[sessionId] = entry;
}
function resetConstitutionWorker(sessionId: string, rules: string[]) {
cleanupConstitution();
constitutionMap[sessionId] = { rules: rules.slice(0, MAX_RULES_PER_SESSION), updated: Date.now() };
}
function getConstitutionWorker(sessionId: string): string[] {
cleanupConstitution();
const entry = constitutionMap[sessionId];
if (!entry) return [];
entry.updated = Date.now();
return entry.rules;
}
// ---------------- KV-backed constitution (Worker) ----------------
const KV_CONS_PREFIX = 'constitution:';
async function kvGetConstitutionEntry(env: Env, sessionId: string): Promise<ConstitutionEntry> {
const key = KV_CONS_PREFIX + sessionId;
try {
const data = await env.VIBE_CONSTITUTION.get(key, 'json');
if (data && typeof data === 'object' && Array.isArray((data as any).rules)) {
return data as ConstitutionEntry;
}
} catch {}
return { rules: [], updated: 0 };
}
async function kvPutConstitutionEntry(env: Env, sessionId: string, entry: ConstitutionEntry): Promise<void> {
const key = KV_CONS_PREFIX + sessionId;
await env.VIBE_CONSTITUTION.put(key, JSON.stringify(entry), { expirationTtl: SESSION_TTL_SEC });
}
async function updateConstitutionKV(env: Env, sessionId: string, rule: string) {
const entry = await kvGetConstitutionEntry(env, sessionId);
if (entry.rules.length >= MAX_RULES_PER_SESSION) entry.rules.shift();
entry.rules.push(rule);
entry.updated = Date.now();
await kvPutConstitutionEntry(env, sessionId, entry);
}
async function resetConstitutionKV(env: Env, sessionId: string, rules: string[]) {
const entry: ConstitutionEntry = { rules: rules.slice(0, MAX_RULES_PER_SESSION), updated: Date.now() };
await kvPutConstitutionEntry(env, sessionId, entry);
}
async function getConstitutionKV(env: Env, sessionId: string): Promise<string[]> {
const entry = await kvGetConstitutionEntry(env, sessionId);
// refresh TTL on read
entry.updated = Date.now();
await kvPutConstitutionEntry(env, sessionId, entry);
return entry.rules;
}