// mcp/src/resources/index.ts
import type { Resource, ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';
import type { SocketClient } from '../client/SocketClient.js';
import type { WhkerDBClient } from '../client/WhkerDBClient.js';
import type { RoomSnapshot, TreeNode } from '@whkerdb/shared';
/**
* 资源 URI 模式
*/
export const RESOURCE_PATTERNS = {
ROOMS: 'whkerdb://rooms',
ROOM: 'whkerdb://rooms/{roomId}',
TREE: 'whkerdb://rooms/{roomId}/tree',
PAGE: 'whkerdb://rooms/{roomId}/pages/{nodeId}',
PDF_INFO: 'whkerdb://pdfs/{pdfId}/info',
};
/**
* 资源模板列表(用于 resources/templates/list)
*/
export function getResourceTemplates(): ResourceTemplate[] {
return [
{
uriTemplate: RESOURCE_PATTERNS.ROOMS,
name: '房间列表',
description: '获取当前已加入的房间列表',
mimeType: 'application/json',
},
{
uriTemplate: RESOURCE_PATTERNS.ROOM,
name: '房间详情',
description: '获取指定房间的详细信息',
mimeType: 'application/json',
},
{
uriTemplate: RESOURCE_PATTERNS.TREE,
name: '笔记树结构',
description: '获取房间的笔记树结构',
mimeType: 'application/json',
},
{
uriTemplate: RESOURCE_PATTERNS.PAGE,
name: '页面内容',
description: '获取指定页面的内容和标注对象',
mimeType: 'application/json',
},
{
uriTemplate: RESOURCE_PATTERNS.PDF_INFO,
name: 'PDF 信息',
description: '获取 PDF 的元信息',
mimeType: 'application/json',
},
];
}
/**
* 资源列表(用于 resources/list)
*
* - 始终提供 `whkerdb://rooms`
* - 如果已加入房间,则额外提供当前房间的详情/树,以及当前快照中的 PDF 资源
*/
export function getResources(getSocketClient: () => SocketClient | null): Resource[] {
const resources: Resource[] = [
{
uri: RESOURCE_PATTERNS.ROOMS,
name: '房间列表',
description: '获取当前已加入的房间列表',
mimeType: 'application/json',
},
];
const socketClient = getSocketClient();
if (!socketClient?.isConnected()) return resources;
const roomId = socketClient.getCurrentRoomId();
const snapshot = socketClient.getSnapshot();
if (roomId) {
resources.push(
{
uri: `whkerdb://rooms/${roomId}`,
name: '房间详情',
description: '获取当前房间的详细信息',
mimeType: 'application/json',
},
{
uri: `whkerdb://rooms/${roomId}/tree`,
name: '笔记树结构',
description: '获取当前房间的笔记树结构',
mimeType: 'application/json',
}
);
}
if (snapshot) {
for (const [pdfId, info] of snapshot.pdfs) {
resources.push({
uri: `whkerdb://pdfs/${pdfId}/info`,
name: 'PDF 信息',
description: `获取 PDF 元信息:${info.filename}`,
mimeType: 'application/json',
});
}
}
return resources;
}
/**
* 辅助函数:从快照中获取节点
*/
function getNodeFromSnapshot(snapshot: RoomSnapshot, nodeId: string): TreeNode | undefined {
const entry = snapshot.noteTree.nodes.find(([id]) => id === nodeId);
return entry?.[1];
}
/**
* 创建资源读取处理器
*/
export function createResourceHandlers(
httpClient: WhkerDBClient,
getSocketClient: () => SocketClient | null
) {
return {
readResource: async (uri: string): Promise<{ contents: Array<{ uri: string; mimeType: string; text: string }> }> => {
const socketClient = getSocketClient();
// whkerdb://rooms
if (uri === RESOURCE_PATTERNS.ROOMS) {
if (!socketClient?.isConnected()) {
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify({ rooms: [], message: '未加入任何房间' }),
},
],
};
}
const roomId = socketClient.getCurrentRoomId();
const snapshot = socketClient.getSnapshot();
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify({
rooms: roomId
? [
{
roomId,
inviteCode: snapshot?.inviteCode,
version: snapshot?.version,
},
]
: [],
}),
},
],
};
}
// whkerdb://rooms/{roomId}
const roomMatch = uri.match(/^whkerdb:\/\/rooms\/([^/]+)$/);
if (roomMatch) {
const roomId = roomMatch[1];
if (!socketClient?.isConnected() || socketClient.getCurrentRoomId() !== roomId) {
// 尝试通过 HTTP API 获取
try {
const info = await httpClient.getRoomInfo(roomId);
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(info),
},
],
};
} catch {
throw new Error(`房间 ${roomId} 不存在或未加入`);
}
}
const snapshot = socketClient.getSnapshot();
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify({
roomId: snapshot?.id,
inviteCode: snapshot?.inviteCode,
version: snapshot?.version,
nodeCount: snapshot?.noteTree.metadata.totalNodes,
pdfCount: snapshot?.pdfs.length,
imageCount: snapshot?.images.length,
sessionCount: snapshot?.sessions.length,
}),
},
],
};
}
// whkerdb://rooms/{roomId}/tree
const treeMatch = uri.match(/^whkerdb:\/\/rooms\/([^/]+)\/tree$/);
if (treeMatch) {
const roomId = treeMatch[1];
if (!socketClient?.isConnected() || socketClient.getCurrentRoomId() !== roomId) {
throw new Error(`未加入房间 ${roomId}`);
}
const snapshot = socketClient.getSnapshot();
if (!snapshot) throw new Error('无法获取房间快照');
const buildTree = (nodeId: string): object => {
const node = getNodeFromSnapshot(snapshot, nodeId);
if (!node) return {};
return {
id: node.id,
type: node.type,
title: node.title,
objectCount: node.objects.length,
children: node.childrenIds.map((childId) => buildTree(childId)),
};
};
const tree = snapshot.noteTree.metadata.rootIds.map((rootId) => buildTree(rootId));
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify({
totalNodes: snapshot.noteTree.metadata.totalNodes,
tree,
}),
},
],
};
}
// whkerdb://rooms/{roomId}/pages/{nodeId}
const pageMatch = uri.match(/^whkerdb:\/\/rooms\/([^/]+)\/pages\/([^/]+)$/);
if (pageMatch) {
const [, roomId, nodeId] = pageMatch;
if (!socketClient?.isConnected() || socketClient.getCurrentRoomId() !== roomId) {
throw new Error(`未加入房间 ${roomId}`);
}
const snapshot = socketClient.getSnapshot();
if (!snapshot) throw new Error('无法获取房间快照');
const node = getNodeFromSnapshot(snapshot, nodeId);
if (!node) throw new Error(`节点 ${nodeId} 不存在`);
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify({
id: node.id,
type: node.type,
title: node.title,
parentId: node.parentId,
childrenIds: node.childrenIds,
objects: node.objects,
...(node.pdfId ? { pdfId: node.pdfId } : {}),
...(node.pageNumber ? { pageNumber: node.pageNumber } : {}),
...(node.width ? { width: node.width } : {}),
...(node.height ? { height: node.height } : {}),
}),
},
],
};
}
// whkerdb://pdfs/{pdfId}/info
const pdfMatch = uri.match(/^whkerdb:\/\/pdfs\/([^/]+)\/info$/);
if (pdfMatch) {
const pdfId = pdfMatch[1];
try {
const info = await httpClient.getPdfInfo(pdfId);
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify({
...info,
fileUrl: httpClient.getPdfFileUrl(pdfId),
}),
},
],
};
} catch {
throw new Error(`PDF ${pdfId} 不存在`);
}
}
throw new Error(`未知的资源 URI: ${uri}`);
},
};
}