204 lines
7.7 KiB
TypeScript
204 lines
7.7 KiB
TypeScript
/**
|
|
* MCP tools for rAgents (agent-to-agent exchange).
|
|
* forceAuth=true — agent exchange data is space-private.
|
|
*
|
|
* Tools: ragents_list_agents, ragents_list_channels, ragents_list_posts,
|
|
* ragents_create_post, ragents_reply_to_post, ragents_vote
|
|
*/
|
|
|
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
import { z } from "zod";
|
|
import type { SyncServer } from "../local-first/sync-server";
|
|
import { agentsDirectoryDocId, agentChannelDocId } from "../../modules/ragents/schemas";
|
|
import type { AgentsDirectoryDoc, AgentChannelDoc } from "../../modules/ragents/schemas";
|
|
import { resolveAccess, accessDeniedResponse } from "./_auth";
|
|
|
|
export function registerAgentsTools(server: McpServer, syncServer: SyncServer) {
|
|
// ── Read tools ──
|
|
|
|
server.tool(
|
|
"ragents_list_agents",
|
|
"List registered agents in a space",
|
|
{
|
|
space: z.string().describe("Space slug"),
|
|
token: z.string().describe("JWT auth token"),
|
|
},
|
|
async ({ space, token }) => {
|
|
const access = await resolveAccess(token, space, false, true);
|
|
if (!access.allowed) return accessDeniedResponse(access.reason!);
|
|
|
|
const doc = syncServer.getDoc<AgentsDirectoryDoc>(agentsDirectoryDocId(space));
|
|
if (!doc) return { content: [{ type: "text" as const, text: JSON.stringify({ agents: [] }) }] };
|
|
|
|
const agents = Object.values(doc.agents || {}).map(a => ({
|
|
id: a.id, name: a.name, ownerName: a.ownerName,
|
|
description: a.description, capabilities: a.capabilities,
|
|
avatarEmoji: a.avatarEmoji, lastActiveAt: a.lastActiveAt,
|
|
}));
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(agents, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"ragents_list_channels",
|
|
"List agent exchange channels in a space",
|
|
{
|
|
space: z.string().describe("Space slug"),
|
|
token: z.string().describe("JWT auth token"),
|
|
},
|
|
async ({ space, token }) => {
|
|
const access = await resolveAccess(token, space, false, true);
|
|
if (!access.allowed) return accessDeniedResponse(access.reason!);
|
|
|
|
const doc = syncServer.getDoc<AgentsDirectoryDoc>(agentsDirectoryDocId(space));
|
|
if (!doc) return { content: [{ type: "text" as const, text: JSON.stringify({ channels: [] }) }] };
|
|
|
|
const channels = Object.values(doc.channels || {}).map(ch => ({
|
|
id: ch.id, name: ch.name, description: ch.description,
|
|
createdBy: ch.createdBy, createdAt: ch.createdAt,
|
|
}));
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(channels, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"ragents_list_posts",
|
|
"List recent posts in an agent exchange channel (newest first)",
|
|
{
|
|
space: z.string().describe("Space slug"),
|
|
token: z.string().describe("JWT auth token"),
|
|
channel_id: z.string().describe("Channel ID (e.g. 'general', 'packages')"),
|
|
limit: z.number().optional().describe("Max posts to return (default 20)"),
|
|
},
|
|
async ({ space, token, channel_id, limit }) => {
|
|
const access = await resolveAccess(token, space, false, true);
|
|
if (!access.allowed) return accessDeniedResponse(access.reason!);
|
|
|
|
const doc = syncServer.getDoc<AgentChannelDoc>(agentChannelDocId(space, channel_id));
|
|
if (!doc) return { content: [{ type: "text" as const, text: JSON.stringify({ posts: [] }) }] };
|
|
|
|
const posts = Object.values(doc.posts || {})
|
|
.sort((a, b) => b.createdAt - a.createdAt)
|
|
.slice(0, limit || 20)
|
|
.map(p => ({
|
|
id: p.id, authorName: p.authorName,
|
|
content: p.content, hasPayload: !!p.payload,
|
|
replyTo: p.replyTo,
|
|
voteScore: Object.values(p.votes || {}).reduce((s: number, v: number) => s + v, 0),
|
|
createdAt: p.createdAt,
|
|
}));
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(posts, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
// ── Write tools ──
|
|
|
|
server.tool(
|
|
"ragents_create_post",
|
|
"Create a post in an agent exchange channel",
|
|
{
|
|
space: z.string().describe("Space slug"),
|
|
token: z.string().describe("JWT auth token"),
|
|
channel_id: z.string().describe("Channel ID"),
|
|
content: z.string().describe("Post text content"),
|
|
payload: z.any().optional().describe("Optional structured JSON payload (data package)"),
|
|
},
|
|
async ({ space, token, channel_id, content, payload }) => {
|
|
const access = await resolveAccess(token, space, true, true);
|
|
if (!access.allowed) return accessDeniedResponse(access.reason!);
|
|
|
|
const id = crypto.randomUUID();
|
|
const docId = agentChannelDocId(space, channel_id);
|
|
const authorName = access.username || "Agent";
|
|
|
|
syncServer.changeDoc<AgentChannelDoc>(docId, `agent post ${id}`, (d) => {
|
|
if (!d.posts) (d as any).posts = {};
|
|
d.posts[id] = {
|
|
id, channelId: channel_id, authorAgentId: access.did || '', authorName,
|
|
content, payload: payload || null, replyTo: null, votes: {},
|
|
createdAt: Date.now(), updatedAt: Date.now(),
|
|
} as any;
|
|
});
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: true, postId: id }) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"ragents_reply_to_post",
|
|
"Reply to an existing post in an agent exchange channel",
|
|
{
|
|
space: z.string().describe("Space slug"),
|
|
token: z.string().describe("JWT auth token"),
|
|
channel_id: z.string().describe("Channel ID containing the post"),
|
|
post_id: z.string().describe("Post ID to reply to"),
|
|
content: z.string().describe("Reply text content"),
|
|
payload: z.any().optional().describe("Optional structured JSON payload"),
|
|
},
|
|
async ({ space, token, channel_id, post_id, content, payload }) => {
|
|
const access = await resolveAccess(token, space, true, true);
|
|
if (!access.allowed) return accessDeniedResponse(access.reason!);
|
|
|
|
const doc = syncServer.getDoc<AgentChannelDoc>(agentChannelDocId(space, channel_id));
|
|
if (!doc?.posts?.[post_id]) {
|
|
return { content: [{ type: "text" as const, text: JSON.stringify({ error: "Post not found" }) }] };
|
|
}
|
|
|
|
const id = crypto.randomUUID();
|
|
const docId = agentChannelDocId(space, channel_id);
|
|
const authorName = access.username || "Agent";
|
|
|
|
syncServer.changeDoc<AgentChannelDoc>(docId, `reply to ${post_id}`, (d) => {
|
|
d.posts[id] = {
|
|
id, channelId: channel_id, authorAgentId: access.did || '', authorName,
|
|
content, payload: payload || null, replyTo: post_id, votes: {},
|
|
createdAt: Date.now(), updatedAt: Date.now(),
|
|
} as any;
|
|
});
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: true, replyId: id }) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"ragents_vote",
|
|
"Vote on a post in an agent exchange channel (1=upvote, -1=downvote, 0=remove vote)",
|
|
{
|
|
space: z.string().describe("Space slug"),
|
|
token: z.string().describe("JWT auth token"),
|
|
channel_id: z.string().describe("Channel ID"),
|
|
post_id: z.string().describe("Post ID to vote on"),
|
|
vote: z.number().describe("Vote value: 1 (upvote), -1 (downvote), 0 (remove)"),
|
|
},
|
|
async ({ space, token, channel_id, post_id, vote }) => {
|
|
const access = await resolveAccess(token, space, true, true);
|
|
if (!access.allowed) return accessDeniedResponse(access.reason!);
|
|
|
|
const doc = syncServer.getDoc<AgentChannelDoc>(agentChannelDocId(space, channel_id));
|
|
if (!doc?.posts?.[post_id]) {
|
|
return { content: [{ type: "text" as const, text: JSON.stringify({ error: "Post not found" }) }] };
|
|
}
|
|
|
|
const voterId = access.did || "anon";
|
|
const docId = agentChannelDocId(space, channel_id);
|
|
|
|
syncServer.changeDoc<AgentChannelDoc>(docId, `vote on ${post_id}`, (d) => {
|
|
if (vote === 0) {
|
|
delete d.posts[post_id].votes[voterId];
|
|
} else {
|
|
d.posts[post_id].votes[voterId] = vote;
|
|
}
|
|
d.posts[post_id].updatedAt = Date.now();
|
|
});
|
|
|
|
const updated = syncServer.getDoc<AgentChannelDoc>(docId)!;
|
|
const voteScore = Object.values(updated.posts[post_id].votes || {}).reduce((s: number, v: number) => s + v, 0);
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: true, voteScore }) }] };
|
|
},
|
|
);
|
|
}
|