diff --git a/mcp/src/index.ts b/mcp/src/index.ts index 73b11ada..3cabcbc8 100644 --- a/mcp/src/index.ts +++ b/mcp/src/index.ts @@ -19,6 +19,10 @@ export default { env: unknown, executionCtx: ExecutionContext, ): Promise { + if (request.method === "OPTIONS") { + return new Response(null, { status: 204, headers: CORS_HEADERS }); + } + let config; try { config = parseConfig(request); @@ -35,6 +39,7 @@ export default { const honcho = createClient(config); const server = createServer({ honcho, config }); const handler = createMcpHandler(server, { + route: "/", corsOptions: { origin: CORS_ORIGIN, methods: CORS_METHODS, diff --git a/mcp/src/tools/conclusions.ts b/mcp/src/tools/conclusions.ts index b44c3b5d..314b603b 100644 --- a/mcp/src/tools/conclusions.ts +++ b/mcp/src/tools/conclusions.ts @@ -149,7 +149,6 @@ export function register(server: McpServer, ctx: ToolContext) { description: [ "Delete a specific conclusion by ID.", "Use this to remove incorrect or outdated knowledge.", - "Returns a confirmation message.", ].join("\n"), inputSchema: { peer_id: z.string().describe("The observer peer."), diff --git a/mcp/src/tools/peers.ts b/mcp/src/tools/peers.ts index a1578916..7a9bb536 100644 --- a/mcp/src/tools/peers.ts +++ b/mcp/src/tools/peers.ts @@ -281,9 +281,7 @@ export function register(server: McpServer, ctx: ToolContext) { "get_peer_metadata", { description: [ - "Get the metadata dictionary for a peer.", - "Use this to read custom attributes stored on a peer.", - "Returns a JSON object of key-value pairs.", + "Get metadata for a peer.", ].join("\n"), inputSchema: { peer_id: z.string().describe("The peer to get metadata for."), @@ -307,9 +305,8 @@ export function register(server: McpServer, ctx: ToolContext) { "set_peer_metadata", { description: [ - "Set metadata for a peer (overwrites existing metadata).", - "Use this to store custom attributes on a peer.", - "Returns a confirmation message.", + "Set metadata for a peer.", + "Overwrites existing metadata.", ].join("\n"), inputSchema: { peer_id: z.string().describe("The peer to set metadata for."), diff --git a/mcp/src/tools/sessions.ts b/mcp/src/tools/sessions.ts index 43e0cc91..baa43bcd 100644 --- a/mcp/src/tools/sessions.ts +++ b/mcp/src/tools/sessions.ts @@ -1,7 +1,14 @@ import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Message } from "@honcho-ai/sdk"; import type { ToolContext } from "../types.js"; -import { textResult, errorResult, formatMessages } from "../types.js"; +import { + textResult, + errorResult, + formatMessage, + formatMessages, + formatSessionSummaries, +} from "../types.js"; export function register(server: McpServer, ctx: ToolContext) { // ── create_session ────────────────────────────────────────────────── @@ -61,9 +68,8 @@ export function register(server: McpServer, ctx: ToolContext) { "delete_session", { description: [ - "Permanently delete a session and all its messages.", - "Use this to clean up conversations that are no longer needed. This cannot be undone.", - "Returns a confirmation message.", + "Delete a session and all its messages.", + "This cannot be undone.", ].join("\n"), inputSchema: { session_id: z.string().describe("The session to delete."), @@ -121,7 +127,6 @@ export function register(server: McpServer, ctx: ToolContext) { description: [ "Add one or more peers to a session.", "Use this to bring participants into a conversation.", - "Returns a confirmation message.", ].join("\n"), inputSchema: { session_id: z.string().describe("The session to add peers to."), @@ -175,8 +180,6 @@ export function register(server: McpServer, ctx: ToolContext) { { description: [ "Remove one or more peers from a session.", - "Use this to remove participants from a conversation.", - "Returns a confirmation message.", ].join("\n"), inputSchema: { session_id: z.string().describe("The session to remove peers from."), @@ -224,6 +227,42 @@ export function register(server: McpServer, ctx: ToolContext) { }, ); + // ── inspect_session ───────────────────────────────────────────────── + server.registerTool( + "inspect_session", + { + description: [ + "Inspect a session at a glance.", + "Aggregates peer IDs, message count, and available summaries.", + "Returns a single JSON object.", + ].join("\n"), + inputSchema: { + session_id: z.string().describe("The session to inspect."), + }, + }, + async ({ session_id }) => { + try { + const session = await ctx.honcho.session(session_id); + const [peers, messagePage, summaries] = await Promise.all([ + session.peers(), + session.messages(), + session.summaries(), + ]); + + return textResult({ + session_id, + peers: peers.map((peer) => ({ id: peer.id })), + message_count: messagePage.total, + summaries: formatSessionSummaries(summaries), + }); + } catch (e) { + return errorResult( + `Failed to inspect session: ${e instanceof Error ? e.message : String(e)}`, + ); + } + }, + ); + // ── add_messages_to_session ───────────────────────────────────────── server.registerTool( "add_messages_to_session", @@ -232,7 +271,6 @@ export function register(server: McpServer, ctx: ToolContext) { "Add messages to a session from specific peers.", "Use this for multi-peer conversations where you need to attribute messages to specific peers.", "For the simple user/assistant flow, use add_turn instead.", - "Returns a confirmation message.", ].join("\n"), inputSchema: { session_id: z.string().describe("The session to add messages to."), @@ -311,6 +349,46 @@ export function register(server: McpServer, ctx: ToolContext) { }, ); + // ── get_session_message ───────────────────────────────────────────── + server.registerTool( + "get_session_message", + { + description: [ + "Get a single message from a session by ID.", + "Use this when you already know the message ID and need the exact record.", + "Returns the message object.", + ].join("\n"), + inputSchema: { + session_id: z.string().describe("The session the message belongs to."), + message_id: z.string().describe("The message ID to fetch."), + }, + }, + async ({ session_id, message_id }) => { + try { + // Workaround: the current @honcho-ai/sdk Session API does not expose + // a single-message getter, so we fetch the message by ID via raw HTTP. + const messageData = await ctx.honcho.http.get<{ + id: string; + content: string; + peer_id: string; + session_id: string; + workspace_id: string; + metadata: Record; + created_at: string; + token_count: number; + }>( + `/v3/workspaces/${ctx.honcho.workspaceId}/sessions/${session_id}/messages/${message_id}`, + ); + const message = Message.fromApiResponse(messageData); + return textResult(formatMessage(message)); + } catch (e) { + return errorResult( + `Failed to get message: ${e instanceof Error ? e.message : String(e)}`, + ); + } + }, + ); + // ── search_session_messages ───────────────────────────────────────── server.registerTool( "search_session_messages", @@ -367,13 +445,7 @@ export function register(server: McpServer, ctx: ToolContext) { return textResult({ session_id: context.sessionId, summary: context.summary, - messages: context.messages.map((msg) => ({ - id: msg.id, - content: msg.content, - peer_id: msg.peerId, - metadata: msg.metadata, - created_at: msg.createdAt, - })), + messages: formatMessages(context.messages), }); } catch (e) { return errorResult( @@ -383,6 +455,32 @@ export function register(server: McpServer, ctx: ToolContext) { }, ); + // ── get_session_summaries ─────────────────────────────────────────── + server.registerTool( + "get_session_summaries", + { + description: [ + "Get the short and long summaries available for a session.", + "Use this when you want Honcho's generated rollups of the conversation so far.", + "Returns the session ID plus short and long summary objects when available.", + ].join("\n"), + inputSchema: { + session_id: z.string().describe("The session to get summaries for."), + }, + }, + async ({ session_id }) => { + try { + const session = await ctx.honcho.session(session_id); + const summaries = await session.summaries(); + return textResult(formatSessionSummaries(summaries)); + } catch (e) { + return errorResult( + `Failed to get session summaries: ${e instanceof Error ? e.message : String(e)}`, + ); + } + }, + ); + // ── get_session_representation ────────────────────────────────────── server.registerTool( "get_session_representation", @@ -425,9 +523,7 @@ export function register(server: McpServer, ctx: ToolContext) { "get_session_metadata", { description: [ - "Get the metadata dictionary for a session.", - "Use this to read custom attributes stored on a session.", - "Returns a JSON object.", + "Get metadata for a session.", ].join("\n"), inputSchema: { session_id: z.string().describe("The session to get metadata for."), @@ -451,9 +547,8 @@ export function register(server: McpServer, ctx: ToolContext) { "set_session_metadata", { description: [ - "Set metadata for a session (overwrites existing metadata).", - "Use this to store custom attributes on a session.", - "Returns a confirmation message.", + "Set metadata for a session.", + "Overwrites existing metadata.", ].join("\n"), inputSchema: { session_id: z.string().describe("The session to set metadata for."), diff --git a/mcp/src/tools/system.ts b/mcp/src/tools/system.ts index c486340a..4c2ba673 100644 --- a/mcp/src/tools/system.ts +++ b/mcp/src/tools/system.ts @@ -12,7 +12,6 @@ export function register(server: McpServer, ctx: ToolContext) { "Schedule a dream — a background memory-consolidation task for a peer.", "Dreams consolidate observations into higher-level insights and update peer cards.", "Use this after a long conversation to improve Honcho's memory quality.", - "Returns a confirmation message.", ].join("\n"), inputSchema: { peer_id: z.string().describe("The observer peer to dream for."), diff --git a/mcp/src/tools/workspace.ts b/mcp/src/tools/workspace.ts index d9f80e5a..c820759d 100644 --- a/mcp/src/tools/workspace.ts +++ b/mcp/src/tools/workspace.ts @@ -4,6 +4,78 @@ import type { ToolContext } from "../types.js"; import { textResult, errorResult, formatMessages } from "../types.js"; export function register(server: McpServer, ctx: ToolContext) { + // ── inspect_workspace ─────────────────────────────────────────────── + server.registerTool( + "inspect_workspace", + { + description: [ + "Inspect the current workspace at a glance.", + "Aggregates workspace metadata, configuration, peer IDs, and session IDs.", + "Returns a single JSON object.", + ].join("\n"), + inputSchema: {}, + }, + async () => { + try { + const [metadata, configuration, peerPage, sessionPage] = await Promise.all([ + ctx.honcho.getMetadata(), + ctx.honcho.getConfiguration(), + ctx.honcho.peers(), + ctx.honcho.sessions(), + ]); + + const peers: { id: string }[] = []; + for await (const peer of peerPage) { + peers.push({ id: peer.id }); + } + + const sessions: { id: string }[] = []; + for await (const session of sessionPage) { + sessions.push({ id: session.id }); + } + + return textResult({ + workspace_id: ctx.honcho.workspaceId, + metadata, + configuration, + peers, + sessions, + }); + } catch (e) { + return errorResult( + `Failed to inspect workspace: ${e instanceof Error ? e.message : String(e)}`, + ); + } + }, + ); + + // ── list_workspaces ───────────────────────────────────────────────── + server.registerTool( + "list_workspaces", + { + description: [ + "List all workspaces accessible to the current credentials.", + "Use this to discover available workspaces before selecting or switching context.", + "Returns an array of workspace IDs.", + ].join("\n"), + inputSchema: {}, + }, + async () => { + try { + const page = await ctx.honcho.workspaces(); + const workspaces: { id: string }[] = []; + for await (const workspace of page) { + workspaces.push({ id: workspace }); + } + return textResult(workspaces); + } catch (e) { + return errorResult( + `Failed to list workspaces: ${e instanceof Error ? e.message : String(e)}`, + ); + } + }, + ); + // ── search_workspace ──────────────────────────────────────────────── server.registerTool( "search_workspace", @@ -34,9 +106,7 @@ export function register(server: McpServer, ctx: ToolContext) { "get_workspace_metadata", { description: [ - "Get the metadata dictionary for the current workspace.", - "Use this to read workspace-level settings or custom attributes.", - "Returns a JSON object of key-value pairs.", + "Get metadata for the current workspace.", ].join("\n"), inputSchema: {}, }, @@ -57,9 +127,8 @@ export function register(server: McpServer, ctx: ToolContext) { "set_workspace_metadata", { description: [ - "Set metadata for the current workspace (overwrites existing metadata).", - "Use this to store workspace-level settings or custom attributes.", - "Returns a confirmation message.", + "Set metadata for the current workspace.", + "Overwrites existing metadata.", ].join("\n"), inputSchema: { metadata: z diff --git a/mcp/src/types.ts b/mcp/src/types.ts index b1b92fb1..413b75b1 100644 --- a/mcp/src/types.ts +++ b/mcp/src/types.ts @@ -1,4 +1,4 @@ -import type { Honcho, Message } from "@honcho-ai/sdk"; +import type { Honcho, Message, Summary, SessionSummaries } from "@honcho-ai/sdk"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { HonchoConfig } from "./config.js"; @@ -18,14 +18,43 @@ export function errorResult(msg: string): CallToolResult { return { content: [{ type: "text", text: msg }], isError: true }; } +/** Serialize a Message to a plain JSON-safe object. */ +export function formatMessage(message: Message) { + return { + id: message.id, + content: message.content, + peer_id: message.peerId, + session_id: message.sessionId, + metadata: message.metadata, + created_at: message.createdAt, + }; +} + +/** Serialize a Summary to a plain JSON-safe object. */ +export function formatSummary(summary: Summary) { + return { + content: summary.content, + message_id: summary.messageId, + summary_type: summary.summaryType, + created_at: summary.createdAt, + token_count: summary.tokenCount, + }; +} + +/** Serialize SessionSummaries to a plain JSON-safe object. */ +export function formatSessionSummaries(summaries: SessionSummaries) { + return { + session_id: summaries.sessionId, + short_summary: summaries.shortSummary + ? formatSummary(summaries.shortSummary) + : null, + long_summary: summaries.longSummary + ? formatSummary(summaries.longSummary) + : null, + }; +} + /** Serialize a Message[] to a plain JSON-safe array. */ export function formatMessages(messages: Message[]) { - return messages.map((m) => ({ - id: m.id, - content: m.content, - peer_id: m.peerId, - session_id: m.sessionId, - metadata: m.metadata, - created_at: m.createdAt, - })); + return messages.map(formatMessage); }