98 lines
3.7 KiB
TypeScript
98 lines
3.7 KiB
TypeScript
/**
|
|
* MCP tools for rChats (multiplayer chat channels).
|
|
* forceAuth=true — chat messages are always sensitive.
|
|
*
|
|
* Tools: rchats_list_channels, rchats_get_channel, rchats_list_messages
|
|
*/
|
|
|
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
import { z } from "zod";
|
|
import type { SyncServer } from "../local-first/sync-server";
|
|
import { chatsDirectoryDocId, chatChannelDocId } from "../../modules/rchats/schemas";
|
|
import type { ChatsDirectoryDoc, ChatChannelDoc } from "../../modules/rchats/schemas";
|
|
import { resolveAccess, accessDeniedResponse } from "./_auth";
|
|
import { filterArrayByVisibility } from "../../shared/membrane";
|
|
|
|
export function registerChatsTools(server: McpServer, syncServer: SyncServer) {
|
|
server.tool(
|
|
"rchats_list_channels",
|
|
"List chat channels in a space",
|
|
{
|
|
space: z.string().describe("Space slug"),
|
|
token: z.string().describe("JWT auth token (required — chat data is private)"),
|
|
},
|
|
async ({ space, token }) => {
|
|
const access = await resolveAccess(token, space, false, true);
|
|
if (!access.allowed) return accessDeniedResponse(access.reason!);
|
|
|
|
const doc = syncServer.getDoc<ChatsDirectoryDoc>(chatsDirectoryDocId(space));
|
|
if (!doc) return { content: [{ type: "text" as const, text: JSON.stringify({ channels: [] }) }] };
|
|
|
|
const channels = filterArrayByVisibility(Object.values(doc.channels || {}), access.role).map(ch => ({
|
|
id: ch.id, name: ch.name, description: ch.description,
|
|
isPrivate: ch.isPrivate, createdBy: ch.createdBy,
|
|
createdAt: ch.createdAt, updatedAt: ch.updatedAt,
|
|
}));
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(channels, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"rchats_get_channel",
|
|
"Get channel details including members",
|
|
{
|
|
space: z.string().describe("Space slug"),
|
|
token: z.string().describe("JWT auth token"),
|
|
channel_id: z.string().describe("Channel ID"),
|
|
},
|
|
async ({ space, token, channel_id }) => {
|
|
const access = await resolveAccess(token, space, false, true);
|
|
if (!access.allowed) return accessDeniedResponse(access.reason!);
|
|
|
|
const doc = syncServer.getDoc<ChatChannelDoc>(chatChannelDocId(space, channel_id));
|
|
if (!doc) return { content: [{ type: "text" as const, text: JSON.stringify({ error: "Channel not found" }) }] };
|
|
|
|
const members = Object.values(doc.members || {});
|
|
const messageCount = Object.keys(doc.messages || {}).length;
|
|
|
|
return {
|
|
content: [{
|
|
type: "text" as const,
|
|
text: JSON.stringify({ channelId: doc.channelId, members, messageCount }, null, 2),
|
|
}],
|
|
};
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"rchats_list_messages",
|
|
"List recent messages in a chat channel (newest first)",
|
|
{
|
|
space: z.string().describe("Space slug"),
|
|
token: z.string().describe("JWT auth token"),
|
|
channel_id: z.string().describe("Channel ID"),
|
|
limit: z.number().optional().describe("Max messages to return (default 50)"),
|
|
},
|
|
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<ChatChannelDoc>(chatChannelDocId(space, channel_id));
|
|
if (!doc) return { content: [{ type: "text" as const, text: JSON.stringify({ error: "Channel not found" }) }] };
|
|
|
|
let messages = filterArrayByVisibility(Object.values(doc.messages || {}), access.role)
|
|
.sort((a, b) => b.createdAt - a.createdAt)
|
|
.slice(0, limit || 50);
|
|
|
|
const result = messages.map(m => ({
|
|
id: m.id, authorName: m.authorName,
|
|
content: m.content, replyTo: m.replyTo,
|
|
editedAt: m.editedAt, createdAt: m.createdAt,
|
|
}));
|
|
|
|
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
},
|
|
);
|
|
}
|