AI Rendezvous MVP: core + HTTP API + SQLite, agent Markdown endpoints, web UI, MCP server
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@ai-rendezvous/mcp",
|
||||
"version": "0.1.0",
|
||||
"description": "MCP server exposing AI Rendezvous tools. An interface to the room transport — NOT an autonomous agent and it cannot wake your model between turns.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "dist/src/index.js",
|
||||
"bin": {
|
||||
"ai-rendezvous-mcp": "dist/src/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-rendezvous/client-sdk": "*",
|
||||
"@ai-rendezvous/core": "*",
|
||||
"@modelcontextprotocol/sdk": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* AI Rendezvous MCP server.
|
||||
*
|
||||
* Scope: this is an INTERFACE to the AI Rendezvous room transport, not an
|
||||
* autonomous participant. It works during your active turn (you call tools,
|
||||
* the server relays to the room's HTTP API). It cannot and does not wake your
|
||||
* model when new messages arrive — polling happens when YOU decide to call
|
||||
* rendezvous_get.
|
||||
*/
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { z } from 'zod';
|
||||
import { RendezvousClient } from '@ai-rendezvous/client-sdk';
|
||||
|
||||
const BASE_URL = (process.env.RENDEZVOUS_BASE_URL ?? 'http://localhost:3000').replace(/\/$/, '');
|
||||
|
||||
const server = new McpServer({
|
||||
name: 'ai-rendezvous',
|
||||
version: '0.1.0',
|
||||
});
|
||||
|
||||
function client(token?: string): RendezvousClient {
|
||||
return new RendezvousClient(BASE_URL, token);
|
||||
}
|
||||
|
||||
const participantSchema = z
|
||||
.object({
|
||||
role: z.string().describe('Short machine-readable role of this participant, e.g. "windows-1c"'),
|
||||
display_name: z.string().optional().describe('Human-readable name'),
|
||||
knows: z.array(z.string()).describe('Facts/systems this participant has direct access to'),
|
||||
needs_to_determine: z.array(z.string()).describe('Things this participant must find out'),
|
||||
instructions: z.string().optional().describe('Extra context/instructions for this participant'),
|
||||
})
|
||||
.describe('A negotiating side (usually an AI session on some machine)');
|
||||
|
||||
server.tool(
|
||||
'rendezvous_create',
|
||||
'Create a temporary negotiation room ("rendezvous") between independent AI sessions (possibly on different machines/harnesses/providers). Returns one SECRET invite URL per participant: the token in the URL is identity AND authorization. Use invite_urls[0] as your own; give invite_urls[1] to the human to forward once to the other AI session. Rooms are ephemeral (≤24h, then all data is deleted).',
|
||||
{
|
||||
title: z.string().describe('What is being negotiated, e.g. "1C <-> app integration contract"'),
|
||||
goal: z.string().optional().describe('Success criterion of the negotiation'),
|
||||
brief: z.string().optional().describe('Background context'),
|
||||
ttl_hours: z.number().int().min(1).max(24).optional().describe('Hours until the room is deleted (default 24)'),
|
||||
participants: z.array(participantSchema).min(2).max(8).describe('The negotiating sides; roles must be unique'),
|
||||
},
|
||||
async (args) => {
|
||||
const created = await client().createRoom(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text:
|
||||
`Room created: ${created.room_id} (expires ${created.expires_at}).\n\n` +
|
||||
created.invite_urls
|
||||
.map((u, i) => `Participant "${created.participants[i].role}" invite URL (secret):\n${u}\nAgent-readable state: ${u}.md`)
|
||||
.join('\n\n') +
|
||||
`\n\nNext: YOU are participant "${created.participants[0].role}" — keep that token. ` +
|
||||
`Give the other invite URL to the human to forward once. Then start by stating verified facts from your side.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'rendezvous_get',
|
||||
'Read the full current state of a rendezvous room: your_role, room_goal, conversation, open questions, current contract draft, room status, and what_you_should_do_next (an explicit instruction computed from negotiation state — follow it). Poll this when you want to check for new messages from the other side.',
|
||||
{
|
||||
token: z.string().describe('Your secret participant token (from your invite URL path /r/<room>/<token>)'),
|
||||
room_id: z.string().describe('Room id'),
|
||||
},
|
||||
async ({ token, room_id }) => {
|
||||
const v = await client(token).getRoom(room_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify(
|
||||
{
|
||||
your_role: v.your_role,
|
||||
room_goal: v.room.goal,
|
||||
room_status: v.room_status,
|
||||
participants: v.participants,
|
||||
open_questions: v.open_questions,
|
||||
current_contract: v.current_contract,
|
||||
what_you_should_do_next: v.what_you_should_do_next,
|
||||
available_actions: v.available_actions,
|
||||
conversation: v.conversation,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'rendezvous_post',
|
||||
'Post a message to the room stating facts you verified on YOUR machine, answers, or arguments. Append-only: history cannot be edited.',
|
||||
{
|
||||
token: z.string().describe('Your secret participant token'),
|
||||
room_id: z.string().describe('Room id'),
|
||||
content: z.string().describe('Message text (markdown). Prefer verified facts over assumptions.'),
|
||||
},
|
||||
async ({ token, room_id, content }) => {
|
||||
const r = await client(token).postMessage(room_id, content);
|
||||
return { content: [{ type: 'text' as const, text: `Message posted: ${JSON.stringify(r)}` }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'rendezvous_ask',
|
||||
'Open a question in the room — ask the other side to verify a fact on their machine, clarify a contradiction, or challenge a contract item. Use blocking=true when the negotiation cannot proceed until it is answered.',
|
||||
{
|
||||
token: z.string().describe('Your secret participant token'),
|
||||
room_id: z.string().describe('Room id'),
|
||||
question: z.string().describe('The question, concretely and verifiably'),
|
||||
blocking: z.boolean().optional().describe('Whether this blocks finalization (default true)'),
|
||||
addressed_to_participant_id: z.string().optional().describe('Participant id the question is for (from rendezvous_get); omit for "anyone"'),
|
||||
},
|
||||
async ({ token, room_id, question, blocking, addressed_to_participant_id }) => {
|
||||
const r = await client(token).ask(room_id, question, { blocking, addressed_to_participant_id });
|
||||
return { content: [{ type: 'text' as const, text: `Question opened: ${JSON.stringify(r)}` }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'rendezvous_resolve',
|
||||
'Mark an open question as resolved. Provide a resolution containing the VERIFIED facts (what you checked, where, what you found) — not just "done". Only the addressee or the author of the question may resolve it.',
|
||||
{
|
||||
token: z.string().describe('Your secret participant token'),
|
||||
room_id: z.string().describe('Room id'),
|
||||
question_id: z.string().describe('Question id from rendezvous_get'),
|
||||
resolution: z.string().describe('The verified answer: what was checked, where, and the result'),
|
||||
},
|
||||
async ({ token, room_id, question_id, resolution }) => {
|
||||
const r = await client(token).resolve(room_id, question_id, resolution);
|
||||
return { content: [{ type: 'text' as const, text: `Question resolved: ${JSON.stringify(r)}` }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'rendezvous_propose_contract',
|
||||
'Propose or revise the structured Agreed Contract (the separate final artifact — not a chat message). Sections: ## Facts, ## Decisions, ## Interface, ## Schedule, ## Authentication, ## Error handling, ## Unresolved. Each proposal creates a new version all participants must agree to.',
|
||||
{
|
||||
token: z.string().describe('Your secret participant token'),
|
||||
room_id: z.string().describe('Room id'),
|
||||
markdown: z.string().describe('Full contract markdown (it replaces the previous version)'),
|
||||
},
|
||||
async ({ token, room_id, markdown }) => {
|
||||
const r = await client(token).proposeContract(room_id, markdown);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: `Contract proposed as version ${r.version}. The other side must review and agree to this version (rendezvous_finalize).`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'rendezvous_finalize',
|
||||
'Agree to the CURRENT contract version. When every participant has agreed to the same version AND no unresolved blocking questions remain, the room status becomes "agreed" and the final Markdown artifact is available. If you disagree, do not call this — post a message and open a blocking question instead.',
|
||||
{
|
||||
token: z.string().describe('Your secret participant token'),
|
||||
room_id: z.string().describe('Room id'),
|
||||
},
|
||||
async ({ token, room_id }) => {
|
||||
const r = await client(token).agree(room_id);
|
||||
const extra = r.room_status === 'agreed' ? ` Room finalized. Final artifact: GET ${BASE_URL}/api/rooms/${room_id}/final.md` : '';
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: `You agreed to contract version ${r.agreed_contract_version}. everyone_agreed=${r.everyone_agreed}, room_status=${r.room_status}.${extra}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ---------- entry point ----------
|
||||
if (process.argv[1] && process.argv[1].endsWith('mcp/dist/src/index.js')) {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error(`AI Rendezvous MCP server running (base url: ${BASE_URL})`);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user