honcho/mcp/src/tools/workspace.ts

290 lines
10 KiB
TypeScript

import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { PageResponse, WorkspaceResponse } from "@honcho-ai/sdk";
import type { ToolContext } from "../types.js";
import {
textResult,
errorResult,
formatMessages,
workspaceIdSchema,
} from "../types.js";
function formatWorkspace(workspace: WorkspaceResponse) {
return {
id: workspace.id,
metadata: workspace.metadata ?? {},
created_at: workspace.created_at,
};
}
export function register(server: McpServer, ctx: ToolContext) {
// ── inspect_workspace ───────────────────────────────────────────────
server.registerTool(
"inspect_workspace",
{
description: [
"Inspect a workspace at a glance.",
"Aggregates workspace metadata, configuration, peer IDs, and session IDs.",
"Returns the first page of peers/sessions with total counts.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
},
},
async ({ workspace_id }) => {
try {
const honcho = ctx.clientFor(workspace_id);
const [metadata, configuration, peerPage, sessionPage] =
await Promise.all([
honcho.getMetadata(),
honcho.getConfiguration(),
honcho.peers(),
honcho.sessions(),
]);
return textResult({
workspace_id: honcho.workspaceId,
metadata,
configuration,
peer_count: peerPage.total,
peers: peerPage.items.map((p) => ({ id: p.id })),
session_count: sessionPage.total,
sessions: sessionPage.items.map((s) => ({ id: s.id })),
});
} catch (e) {
return errorResult(
`Failed to inspect workspace: ${e instanceof Error ? e.message : String(e)}`,
);
}
},
);
// ── list_workspaces ─────────────────────────────────────────────────
server.registerTool(
"list_workspaces",
{
description: [
"List workspaces accessible to the current credentials (paginated).",
"Skip this if the connection already set X-Honcho-Workspace-ID — that header is the workspace; omit workspace_id on other tools.",
"Use this only when the header is unset and you don't already know the workspace ID.",
"Returns each workspace's id, metadata, and created_at. If none fit, call create_workspace.",
].join("\n"),
inputSchema: {
page: z
.number()
.int()
.min(1)
.optional()
.describe("Page number (1-indexed)."),
size: z
.number()
.int()
.min(1)
.max(100)
.optional()
.describe("Results per page (max 100)."),
},
},
async ({ page, size }) => {
try {
const result = await ctx.unscoped.http.post<
PageResponse<WorkspaceResponse>
>("/v3/workspaces/list", {
body: {},
query: { page, size },
});
return textResult({
workspaces: result.items.map(formatWorkspace),
total: result.total,
page: result.page,
pages: result.pages,
});
} catch (e) {
return errorResult(
`Failed to list workspaces: ${e instanceof Error ? e.message : String(e)}`,
);
}
},
);
// ── create_workspace ────────────────────────────────────────────────
server.registerTool(
"create_workspace",
{
description: [
"Get or create a workspace with the given ID.",
"Skip this if the connection already set X-Honcho-Workspace-ID — that header pins the workspace without a create call.",
"Use this only when the header is unset and list_workspaces has no suitable workspace.",
"Optional metadata helps future list_workspaces calls identify what the workspace is for.",
"Returns the workspace id, metadata, and created_at.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
metadata: z
.record(z.string(), z.unknown())
.optional()
.describe(
"Optional key-value metadata to store on the workspace (e.g. project, purpose).",
),
},
},
async ({ workspace_id, metadata }) => {
try {
const workspace = await ctx.unscoped.http.post<WorkspaceResponse>(
"/v3/workspaces",
{
body: {
id: workspace_id,
metadata,
},
},
);
return textResult(formatWorkspace(workspace));
} catch (e) {
return errorResult(
`Failed to create workspace: ${e instanceof Error ? e.message : String(e)}`,
);
}
},
);
// ── search ────────────────────────────────────────────────────────
server.registerTool(
"search",
{
description: [
"Semantic search across messages. Scope is determined by which optional params are provided:",
"- No scope params: search all messages in the workspace.",
"- peer_id only: search messages authored by that peer across all sessions.",
"- session_id only: search messages within that session.",
"Returns an array of matching messages with their content, peer, and session info.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
query: z.string().describe("Search query."),
peer_id: z
.string()
.optional()
.describe("Optional: scope search to messages by this peer."),
session_id: z
.string()
.optional()
.describe("Optional: scope search to messages in this session."),
},
},
async ({ workspace_id, query, peer_id, session_id }) => {
try {
const honcho = ctx.clientFor(workspace_id);
let messages;
if (session_id) {
const session = await honcho.session(session_id);
messages = await session.search(query);
} else if (peer_id) {
const peer = await honcho.peer(peer_id);
messages = await peer.search(query);
} else {
messages = await honcho.search(query);
}
return textResult(formatMessages(messages));
} catch (e) {
return errorResult(
`Search failed: ${e instanceof Error ? e.message : String(e)}`,
);
}
},
);
// ── get_metadata ──────────────────────────────────────────────────
server.registerTool(
"get_metadata",
{
description: [
"Get metadata for a resource. Scope is determined by which optional params are provided:",
"- No scope params: get workspace metadata.",
"- peer_id only: get peer metadata.",
"- session_id only: get session metadata.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
peer_id: z
.string()
.optional()
.describe("Optional: get metadata for this peer."),
session_id: z
.string()
.optional()
.describe("Optional: get metadata for this session."),
},
},
async ({ workspace_id, peer_id, session_id }) => {
try {
const honcho = ctx.clientFor(workspace_id);
let metadata;
if (session_id) {
const session = await honcho.session(session_id);
metadata = await session.getMetadata();
} else if (peer_id) {
const peer = await honcho.peer(peer_id);
metadata = await peer.getMetadata();
} else {
metadata = await honcho.getMetadata();
}
return textResult(metadata);
} catch (e) {
return errorResult(
`Failed to get metadata: ${e instanceof Error ? e.message : String(e)}`,
);
}
},
);
// ── set_metadata ──────────────────────────────────────────────────
server.registerTool(
"set_metadata",
{
description: [
"Set metadata for a resource. Overwrites existing metadata.",
"Scope is determined by which optional params are provided:",
"- No scope params: set workspace metadata.",
"- peer_id only: set peer metadata.",
"- session_id only: set session metadata.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
metadata: z
.record(z.string(), z.unknown())
.describe("Key-value pairs to set as metadata."),
peer_id: z
.string()
.optional()
.describe("Optional: set metadata for this peer."),
session_id: z
.string()
.optional()
.describe("Optional: set metadata for this session."),
},
},
async ({ workspace_id, metadata, peer_id, session_id }) => {
try {
const honcho = ctx.clientFor(workspace_id);
if (session_id) {
const session = await honcho.session(session_id);
await session.setMetadata(metadata);
return textResult("Session metadata set successfully");
} else if (peer_id) {
const peer = await honcho.peer(peer_id);
await peer.setMetadata(metadata);
return textResult("Peer metadata set successfully");
} else {
await honcho.setMetadata(metadata);
return textResult("Workspace metadata set successfully");
}
} catch (e) {
return errorResult(
`Failed to set metadata: ${e instanceof Error ? e.message : String(e)}`,
);
}
},
);
}