feat: adds workspace headers to MCP calls and adds list/create workspace tools (#1020)

* feat: adds workspace headers to MCP calls and adds list/create workspace tools

* docs: clean

* fix(mcp): clarify missing-workspace and admin-key errors

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ajspig 2026-08-13 16:48:31 -04:00 committed by GitHub
parent b7d4a2a3ad
commit e82d2ef497
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 348 additions and 142 deletions

View File

@ -239,38 +239,11 @@ To teach Goose the recommended memory flow, save the [instructions](https://raw.
---
## Optional Configuration
## Workspace
You can target a specific workspace by adding an extra header. It's optional.
Every workspace-scoped tool takes a `workspace_id` argument. You can also set `X-Honcho-Workspace-ID` on the connection; that value fills `workspace_id` when the argument is omitted.
| Header | Default | Description |
|--------|---------|-------------|
| `Authorization` | *required* | `Bearer hch-your-key-here` |
| `X-Honcho-Workspace-ID` | `"default"` | Isolate memory per project |
Example with all headers (Claude Desktop format):
```json
{
"mcpServers": {
"honcho": {
"command": "npx",
"args": [
"mcp-remote",
"https://mcp.honcho.dev",
"--header",
"Authorization:${AUTH_HEADER}",
"--header",
"X-Honcho-Workspace-ID:${WORKSPACE_ID}"
],
"env": {
"AUTH_HEADER": "Bearer hch-your-key-here",
"WORKSPACE_ID": "my-project"
}
}
}
}
```
Use `list_workspaces` to discover IDs (each result includes metadata and `created_at`), or `create_workspace` if none fit, then reuse the same ID for subsequent tool calls.
---

View File

@ -26,15 +26,11 @@ A Cloudflare Worker that implements the [Model Context Protocol (MCP)](https://m
}
```
### Optional Headers
| Header | Default | Description |
| --- | --- | --- |
| `X-Honcho-Workspace-ID` | `"default"` | Workspace to operate in |
Every workspace-scoped tool takes a `workspace_id` argument. If you set `X-Honcho-Workspace-ID` on the connection, that value fills `workspace_id` when the argument is omitted. Use `list_workspaces` to discover IDs.
## Available Tools
**Workspace:** `inspect_workspace` (aggregates metadata, configuration, and peer/session IDs), `list_workspaces` (enumerates accessible workspaces), `search` (semantic search scoped by optional peer/session params), `get_metadata`, `set_metadata`
**Workspace:** `list_workspaces` (id, metadata, created_at), `create_workspace` (get-or-create with optional metadata), `inspect_workspace` (aggregates metadata, configuration, and peer/session IDs), `search` (semantic search scoped by optional peer/session params), `get_metadata`, `set_metadata`
**Peers:** `create_peer`, `list_peers`, `chat`, `get_peer_card`, `set_peer_card`, `get_peer_context`, `get_representation`
@ -50,7 +46,7 @@ A Cloudflare Worker that implements the [Model Context Protocol (MCP)](https://m
src/
index.ts # Worker entry point — parse config, delegate to MCP handler
server.ts # createServer() — registers all tools on an McpServer
config.ts # HonchoConfig, parseConfig(), createClient()
config.ts # HonchoConfig, parseConfig(), createClientFactory()
types.ts # ToolContext, result helpers
tools/
workspace.ts # inspect, list, search, metadata

View File

@ -4,12 +4,21 @@
The simplest way to use Honcho for a standard user/assistant conversation. Three steps using the general tools.
Every workspace-scoped tool takes `workspace_id`. The simplest setup is for the client to set `X-Honcho-Workspace-ID` on the connection — then omit `workspace_id` on every call. Do not list or create a workspace just to rediscover a header that is already set.
If the header is unset and you don't already know the workspace:
1. Call `list_workspaces` and pick the workspace whose id or metadata best matches this work.
2. If none fit, call `create_workspace` with a descriptive id (and optional metadata like `{ "project": "...", "purpose": "..." }`).
3. Reuse that same `workspace_id` for the rest of the conversation.
### 1. Start a conversation (once per conversation)
Create a session and set up the user and assistant peers:
```
create_session
workspace_id: "<workspace-id>"
session_id: "<unique-id>"
```
@ -17,12 +26,15 @@ Then add peers to the session:
```
create_peer
workspace_id: "<workspace-id>"
peer_id: "<user-name>"
create_peer
workspace_id: "<workspace-id>"
peer_id: "Assistant"
add_peers_to_session
workspace_id: "<workspace-id>"
session_id: "<session_id>"
peers:
- peer_id: "<user-name>"
@ -39,6 +51,7 @@ Store the `session_id` for the rest of this conversation.
```
chat
workspace_id: "<workspace-id>"
peer_id: "Assistant"
query: "What communication style does this user prefer?"
target_peer_id: "<user-name>"
@ -58,6 +71,7 @@ This calls Honcho's reasoning system to answer your question about the user, gro
```
add_messages_to_session
workspace_id: "<workspace-id>"
session_id: "<session_id>"
messages:
- peer_id: "<user-name>"
@ -88,8 +102,9 @@ The full API for advanced use cases.
| Tool | When to use |
| --- | --- |
| `inspect_workspace` | Inspect a single workspace's details |
| `list_workspaces` | Enumerate available workspaces |
| `list_workspaces` | Discover available workspaces (id, metadata, created_at). No `workspace_id` needed. |
| `create_workspace` | Get or create a workspace when none of the listed ones fit |
| `inspect_workspace` | Inspect a single workspace's details. Requires `workspace_id`. |
| `search` | Semantic search across messages — scope with optional `peer_id` or `session_id` params |
| `get_metadata` | Read metadata for workspace, peer, or session (scope with optional `peer_id` or `session_id`) |
| `set_metadata` | Store metadata for workspace, peer, or session (scope with optional `peer_id` or `session_id`) |

View File

@ -21,7 +21,7 @@
},
{
"name": "X-Honcho-Workspace-ID",
"description": "Optional. Target Honcho workspace; defaults to 'default' when omitted.",
"description": "Optional. Default Honcho workspace for tool calls. When set, it fills workspace_id on tools.",
"isRequired": false,
"isSecret": false
}

View File

@ -3,7 +3,8 @@ import { Honcho } from "@honcho-ai/sdk";
export interface HonchoConfig {
apiKey: string;
baseUrl: string;
workspaceId: string;
/** From X-Honcho-Workspace-ID when set. */
workspaceId?: string;
}
export interface Env {
@ -19,6 +20,9 @@ export interface Env {
* instance (see the "Self-Hosted Honcho" section in README.md). It is
* intentionally not exposed as a request header: routing public requests
* to an internal URL would be a latency and security regression.
*
* Optional `X-Honcho-Workspace-ID` becomes the default `workspace_id` on
* tools. If the header is omitted, each tool call must pass `workspace_id`.
*/
export function parseConfig(request: Request, env: Env = {}): HonchoConfig {
const authHeader = request.headers.get("Authorization");
@ -33,17 +37,60 @@ export function parseConfig(request: Request, env: Env = {}): HonchoConfig {
throw new Error("Authorization header is empty after 'Bearer '.");
}
const workspaceId =
request.headers.get("X-Honcho-Workspace-ID")?.trim() || undefined;
return {
apiKey,
baseUrl: env.HONCHO_API_URL?.trim() || "https://api.honcho.dev",
workspaceId: request.headers.get("X-Honcho-Workspace-ID")?.trim() || "default",
workspaceId,
};
}
export function createClient(config: HonchoConfig): Honcho {
export const MISSING_WORKSPACE_ID_MESSAGE =
"Missing workspace_id. Pass workspace_id on the next tool call, or set the X-Honcho-Workspace-ID header on the connection so it is used automatically.";
export function resolveWorkspaceId(
config: HonchoConfig,
workspaceId?: string,
): string {
const id = workspaceId?.trim() || config.workspaceId?.trim();
if (!id) {
throw new Error(MISSING_WORKSPACE_ID_MESSAGE);
}
return id;
}
export function createClient(
config: HonchoConfig,
workspaceId: string,
): Honcho {
return new Honcho({
apiKey: config.apiKey,
baseURL: config.baseUrl,
workspaceId: config.workspaceId,
workspaceId,
});
}
/** Client used only for credential-scoped ops (list workspaces). */
export function createUnscopedClient(config: HonchoConfig): Honcho {
return new Honcho({
apiKey: config.apiKey,
baseURL: config.baseUrl,
});
}
export function createClientFactory(
config: HonchoConfig,
): (workspaceId?: string) => Honcho {
const cache = new Map<string, Honcho>();
return (workspaceId?: string) => {
const id = resolveWorkspaceId(config, workspaceId);
let client = cache.get(id);
if (!client) {
client = createClient(config, id);
cache.set(id, client);
}
return client;
};
}

View File

@ -1,5 +1,10 @@
import { createMcpHandler } from "agents/mcp";
import { parseConfig, createClient, type Env } from "./config.js";
import {
parseConfig,
createClientFactory,
createUnscopedClient,
type Env,
} from "./config.js";
import { createServer } from "./server.js";
const CORS_ORIGIN = "*";
@ -66,8 +71,11 @@ export default {
}
try {
const honcho = createClient(config);
const server = createServer({ honcho, config });
const server = createServer({
config,
clientFor: createClientFactory(config),
unscoped: createUnscopedClient(config),
});
const handler = createMcpHandler(server, {
route: "/",
corsOptions: {

View File

@ -1,7 +1,7 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { ToolContext } from "../types.js";
import { textResult, errorResult } from "../types.js";
import { textResult, errorResult, workspaceIdSchema } from "../types.js";
export function register(server: McpServer, ctx: ToolContext) {
// ── list_conclusions ────────────────────────────────────────────────
@ -14,6 +14,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns conclusion objects with pagination metadata.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
peer_id: z.string().describe("The observer peer."),
target_peer_id: z
.string()
@ -23,9 +24,9 @@ export function register(server: McpServer, ctx: ToolContext) {
),
},
},
async ({ peer_id, target_peer_id }) => {
async ({ workspace_id, peer_id, target_peer_id }) => {
try {
const peer = await ctx.honcho.peer(peer_id);
const peer = await ctx.clientFor(workspace_id).peer(peer_id);
const scope = target_peer_id
? peer.conclusionsOf(target_peer_id)
: peer.conclusions;
@ -61,6 +62,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns an array of matching conclusions ranked by relevance.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
peer_id: z.string().describe("The observer peer."),
query: z.string().describe("Semantic search query."),
target_peer_id: z
@ -79,9 +81,9 @@ export function register(server: McpServer, ctx: ToolContext) {
),
},
},
async ({ peer_id, query, target_peer_id, top_k, filters }) => {
async ({ workspace_id, peer_id, query, target_peer_id, top_k, filters }) => {
try {
const peer = await ctx.honcho.peer(peer_id);
const peer = await ctx.clientFor(workspace_id).peer(peer_id);
const scope = target_peer_id
? peer.conclusionsOf(target_peer_id)
: peer.conclusions;
@ -115,6 +117,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns the number of conclusions created.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
peer_id: z.string().describe("The observer peer."),
target_peer_id: z
.string()
@ -130,9 +133,15 @@ export function register(server: McpServer, ctx: ToolContext) {
),
},
},
async ({ peer_id, target_peer_id, conclusions, session_id }) => {
async ({
workspace_id,
peer_id,
target_peer_id,
conclusions,
session_id,
}) => {
try {
const peer = await ctx.honcho.peer(peer_id);
const peer = await ctx.clientFor(workspace_id).peer(peer_id);
const scope = peer.conclusionsOf(target_peer_id);
const params = conclusions.map((content) => ({
content,
@ -160,6 +169,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Use this to remove incorrect or outdated knowledge.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
peer_id: z.string().describe("The observer peer."),
target_peer_id: z
.string()
@ -167,9 +177,9 @@ export function register(server: McpServer, ctx: ToolContext) {
conclusion_id: z.string().describe("The conclusion to delete."),
},
},
async ({ peer_id, target_peer_id, conclusion_id }) => {
async ({ workspace_id, peer_id, target_peer_id, conclusion_id }) => {
try {
const peer = await ctx.honcho.peer(peer_id);
const peer = await ctx.clientFor(workspace_id).peer(peer_id);
const scope = peer.conclusionsOf(target_peer_id);
await scope.delete(conclusion_id);
return textResult("Conclusion deleted successfully");

View File

@ -1,7 +1,7 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { ToolContext } from "../types.js";
import { textResult, errorResult } from "../types.js";
import { textResult, errorResult, workspaceIdSchema } from "../types.js";
export function register(server: McpServer, ctx: ToolContext) {
// ── create_peer ─────────────────────────────────────────────────────
@ -14,6 +14,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns the peer ID and any configuration that was set.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
peer_id: z.string().describe("Unique identifier for the peer."),
configuration: z
.object({
@ -25,9 +26,11 @@ export function register(server: McpServer, ctx: ToolContext) {
.describe("Optional peer configuration."),
},
},
async ({ peer_id, configuration }) => {
async ({ workspace_id, peer_id, configuration }) => {
try {
const peer = await ctx.honcho.peer(peer_id, { configuration });
const peer = await ctx.clientFor(workspace_id).peer(peer_id, {
configuration,
});
return textResult({ peer_id: peer.id, configuration: peer.configuration });
} catch (e) {
return errorResult(
@ -42,15 +45,17 @@ export function register(server: McpServer, ctx: ToolContext) {
"list_peers",
{
description: [
"List peers in the current workspace (paginated).",
"List peers in the given workspace (paginated).",
"Use this to discover which users and agents exist.",
"Returns peer IDs with pagination metadata.",
].join("\n"),
inputSchema: {},
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
},
},
async () => {
async ({ workspace_id }) => {
try {
const page = await ctx.honcho.peers();
const page = await ctx.clientFor(workspace_id).peers();
return textResult({
peers: page.items.map((p) => ({ id: p.id })),
total: page.total,
@ -75,6 +80,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns a natural-language answer, or 'None' if no relevant information exists.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
peer_id: z.string().describe("The peer to query about."),
query: z.string().describe("Natural-language question."),
target_peer_id: z
@ -93,9 +99,16 @@ export function register(server: McpServer, ctx: ToolContext) {
.describe("Reasoning effort. Higher = more detailed but slower."),
},
},
async ({ peer_id, query, target_peer_id, session_id, reasoning_level }) => {
async ({
workspace_id,
peer_id,
query,
target_peer_id,
session_id,
reasoning_level,
}) => {
try {
const peer = await ctx.honcho.peer(peer_id);
const peer = await ctx.clientFor(workspace_id).peer(peer_id);
const result = await peer.chat(query, {
target: target_peer_id,
session: session_id,
@ -120,6 +133,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns an array of fact strings, or null if no card exists yet.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
peer_id: z.string().describe("The observer peer."),
target_peer_id: z
.string()
@ -129,9 +143,9 @@ export function register(server: McpServer, ctx: ToolContext) {
),
},
},
async ({ peer_id, target_peer_id }) => {
async ({ workspace_id, peer_id, target_peer_id }) => {
try {
const peer = await ctx.honcho.peer(peer_id);
const peer = await ctx.clientFor(workspace_id).peer(peer_id);
const card = await peer.getCard(target_peer_id);
return textResult(card ?? "No peer card found.");
} catch (e) {
@ -152,6 +166,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns the updated peer card.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
peer_id: z.string().describe("The observer peer."),
peer_card: z
.array(z.string())
@ -164,9 +179,9 @@ export function register(server: McpServer, ctx: ToolContext) {
),
},
},
async ({ peer_id, peer_card, target_peer_id }) => {
async ({ workspace_id, peer_id, peer_card, target_peer_id }) => {
try {
const peer = await ctx.honcho.peer(peer_id);
const peer = await ctx.clientFor(workspace_id).peer(peer_id);
const result = await peer.setCard(peer_card, target_peer_id);
return textResult(result ?? "Peer card set successfully");
} catch (e) {
@ -187,6 +202,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns an object with representation, peer_card, peer_id, and target_id.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
peer_id: z.string().describe("The observer peer."),
target_peer_id: z
.string()
@ -202,9 +218,15 @@ export function register(server: McpServer, ctx: ToolContext) {
.describe("Optional: max number of conclusions to include."),
},
},
async ({ peer_id, target_peer_id, search_query, max_conclusions }) => {
async ({
workspace_id,
peer_id,
target_peer_id,
search_query,
max_conclusions,
}) => {
try {
const peer = await ctx.honcho.peer(peer_id);
const peer = await ctx.clientFor(workspace_id).peer(peer_id);
const context = await peer.context({
target: target_peer_id,
searchQuery: search_query,
@ -234,6 +256,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns a formatted string of conclusions.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
peer_id: z.string().describe("The observer peer."),
target_peer_id: z
.string()
@ -254,6 +277,7 @@ export function register(server: McpServer, ctx: ToolContext) {
},
},
async ({
workspace_id,
peer_id,
target_peer_id,
session_id,
@ -261,7 +285,7 @@ export function register(server: McpServer, ctx: ToolContext) {
max_conclusions,
}) => {
try {
const peer = await ctx.honcho.peer(peer_id);
const peer = await ctx.clientFor(workspace_id).peer(peer_id);
const rep = await peer.representation({
target: target_peer_id,
session: session_id,

View File

@ -7,6 +7,7 @@ import {
formatMessage,
formatMessages,
formatSessionSummaries,
workspaceIdSchema,
} from "../types.js";
export function register(server: McpServer, ctx: ToolContext) {
@ -20,12 +21,13 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns the session ID.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
session_id: z.string().describe("Unique identifier for the session."),
},
},
async ({ session_id }) => {
async ({ workspace_id, session_id }) => {
try {
const session = await ctx.honcho.session(session_id);
const session = await ctx.clientFor(workspace_id).session(session_id);
return textResult({ session_id: session.id });
} catch (e) {
return errorResult(
@ -40,15 +42,17 @@ export function register(server: McpServer, ctx: ToolContext) {
"list_sessions",
{
description: [
"List sessions in the current workspace (paginated).",
"List sessions in the given workspace (paginated).",
"Use this to discover existing conversations.",
"Returns session IDs with pagination metadata.",
].join("\n"),
inputSchema: {},
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
},
},
async () => {
async ({ workspace_id }) => {
try {
const page = await ctx.honcho.sessions();
const page = await ctx.clientFor(workspace_id).sessions();
return textResult({
sessions: page.items.map((s) => ({ id: s.id })),
total: page.total,
@ -72,12 +76,13 @@ export function register(server: McpServer, ctx: ToolContext) {
"This cannot be undone.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
session_id: z.string().describe("The session to delete."),
},
},
async ({ session_id }) => {
async ({ workspace_id, session_id }) => {
try {
const session = await ctx.honcho.session(session_id);
const session = await ctx.clientFor(workspace_id).session(session_id);
await session.delete();
return textResult("Session deleted successfully");
} catch (e) {
@ -98,6 +103,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns the new cloned session ID.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
session_id: z.string().describe("The session to clone."),
message_id: z
.string()
@ -107,9 +113,9 @@ export function register(server: McpServer, ctx: ToolContext) {
),
},
},
async ({ session_id, message_id }) => {
async ({ workspace_id, session_id, message_id }) => {
try {
const session = await ctx.honcho.session(session_id);
const session = await ctx.clientFor(workspace_id).session(session_id);
const cloned = await session.clone(message_id);
return textResult({ session_id: cloned.id });
} catch (e) {
@ -129,6 +135,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Use this to bring participants into a conversation.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
session_id: z.string().describe("The session to add peers to."),
peers: z
.array(
@ -152,9 +159,10 @@ export function register(server: McpServer, ctx: ToolContext) {
.describe("Peers to add — plain IDs or objects with per-session config."),
},
},
async ({ session_id, peers }) => {
async ({ workspace_id, session_id, peers }) => {
try {
const session = await ctx.honcho.session(session_id);
const honcho = ctx.clientFor(workspace_id);
const session = await honcho.session(session_id);
const additions = peers.map((p) => {
if (typeof p === "string") return p;
const config: { observeMe?: boolean | null; observeOthers?: boolean | null } = {};
@ -182,15 +190,16 @@ export function register(server: McpServer, ctx: ToolContext) {
"Remove one or more peers from a session.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
session_id: z.string().describe("The session to remove peers from."),
peer_ids: z
.array(z.string())
.describe("Peer IDs to remove."),
},
},
async ({ session_id, peer_ids }) => {
async ({ workspace_id, session_id, peer_ids }) => {
try {
const session = await ctx.honcho.session(session_id);
const session = await ctx.clientFor(workspace_id).session(session_id);
await session.removePeers(peer_ids);
return textResult("Peers removed from session successfully");
} catch (e) {
@ -211,12 +220,13 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns an array of peer IDs.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
session_id: z.string().describe("The session to query."),
},
},
async ({ session_id }) => {
async ({ workspace_id, session_id }) => {
try {
const session = await ctx.honcho.session(session_id);
const session = await ctx.clientFor(workspace_id).session(session_id);
const peers = await session.peers();
return textResult(peers.map((p) => p.id));
} catch (e) {
@ -237,12 +247,13 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns a single JSON object.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
session_id: z.string().describe("The session to inspect."),
},
},
async ({ session_id }) => {
async ({ workspace_id, session_id }) => {
try {
const session = await ctx.honcho.session(session_id);
const session = await ctx.clientFor(workspace_id).session(session_id);
const [peers, messagePage, summaries] = await Promise.all([
session.peers(),
session.messages(),
@ -273,6 +284,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Each message must specify the peer_id of the author.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
session_id: z.string().describe("The session to add messages to."),
messages: z
.array(
@ -288,15 +300,16 @@ export function register(server: McpServer, ctx: ToolContext) {
.describe("Messages to add."),
},
},
async ({ session_id, messages }) => {
async ({ workspace_id, session_id, messages }) => {
try {
const session = await ctx.honcho.session(session_id);
const peerCache = new Map<string, Awaited<ReturnType<typeof ctx.honcho.peer>>>();
const honcho = ctx.clientFor(workspace_id);
const session = await honcho.session(session_id);
const peerCache = new Map<string, Awaited<ReturnType<typeof honcho.peer>>>();
const sessionMessages = [];
for (const msg of messages) {
let peer = peerCache.get(msg.peer_id);
if (!peer) {
peer = await ctx.honcho.peer(msg.peer_id);
peer = await honcho.peer(msg.peer_id);
peerCache.set(msg.peer_id, peer);
}
sessionMessages.push(
@ -325,6 +338,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns the first page of messages with pagination metadata.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
session_id: z.string().describe("The session to get messages from."),
filters: z
.record(z.string(), z.unknown())
@ -332,9 +346,9 @@ export function register(server: McpServer, ctx: ToolContext) {
.describe("Optional metadata filter criteria."),
},
},
async ({ session_id, filters }) => {
async ({ workspace_id, session_id, filters }) => {
try {
const session = await ctx.honcho.session(session_id);
const session = await ctx.clientFor(workspace_id).session(session_id);
const page = await session.messages(filters);
return textResult({
messages: formatMessages(page.items),
@ -360,13 +374,14 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns the message object.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
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 }) => {
async ({ workspace_id, session_id, message_id }) => {
try {
const session = await ctx.honcho.session(session_id);
const session = await ctx.clientFor(workspace_id).session(session_id);
const message = await session.getMessage(message_id);
return textResult(formatMessage(message));
} catch (e) {
@ -388,6 +403,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns messages, summary, and session ID.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
session_id: z.string().describe("The session to get context for."),
summary: z
.boolean()
@ -399,9 +415,9 @@ export function register(server: McpServer, ctx: ToolContext) {
.describe("Target token budget for the context window."),
},
},
async ({ session_id, summary, tokens }) => {
async ({ workspace_id, session_id, summary, tokens }) => {
try {
const session = await ctx.honcho.session(session_id);
const session = await ctx.clientFor(workspace_id).session(session_id);
const context = await session.context({ summary, tokens });
return textResult({
session_id: context.sessionId,

View File

@ -1,7 +1,7 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { ToolContext } from "../types.js";
import { textResult, errorResult } from "../types.js";
import { textResult, errorResult, workspaceIdSchema } from "../types.js";
export function register(server: McpServer, ctx: ToolContext) {
// ── schedule_dream ──────────────────────────────────────────────────
@ -14,6 +14,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Use this after a long conversation to improve Honcho's memory quality.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
peer_id: z.string().describe("The observer peer to dream for."),
target_peer_id: z
.string()
@ -27,9 +28,9 @@ export function register(server: McpServer, ctx: ToolContext) {
.describe("Optional: scope the dream to a session."),
},
},
async ({ peer_id, target_peer_id, session_id }) => {
async ({ workspace_id, peer_id, target_peer_id, session_id }) => {
try {
await ctx.honcho.scheduleDream({
await ctx.clientFor(workspace_id).scheduleDream({
observer: peer_id,
observed: target_peer_id,
session: session_id,
@ -52,11 +53,13 @@ export function register(server: McpServer, ctx: ToolContext) {
"Use this to check if Honcho is still processing messages before querying for insights.",
"Returns work unit counts: total, completed, in-progress, and pending.",
].join("\n"),
inputSchema: {},
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
},
},
async () => {
async ({ workspace_id }) => {
try {
const status = await ctx.honcho.queueStatus();
const status = await ctx.clientFor(workspace_id).queueStatus();
return textResult(status);
} catch (e) {
return errorResult(

View File

@ -1,8 +1,36 @@
import { z } from "zod";
import { BadRequestError, UnprocessableEntityError } from "@honcho-ai/sdk";
import {
BadRequestError,
HonchoError,
UnprocessableEntityError,
type PageResponse,
type WorkspaceResponse,
} from "@honcho-ai/sdk";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { ToolContext } from "../types.js";
import { textResult, errorResult, formatMessages } from "../types.js";
import { resolveWorkspaceId } from "../config.js";
import {
textResult,
errorResult,
formatMessages,
workspaceIdSchema,
} from "../types.js";
function withAdminKeyHint(prefix: string, e: unknown): string {
const message = e instanceof Error ? e.message : String(e);
const denied =
e instanceof HonchoError && (e.status === 401 || e.status === 403);
if (!denied) return `${prefix}: ${message}`;
return `${prefix}: ${message}. This operation is only possible with an admin API key.`;
}
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 ───────────────────────────────────────────────
@ -10,23 +38,27 @@ export function register(server: McpServer, ctx: ToolContext) {
"inspect_workspace",
{
description: [
"Inspect the current workspace at a glance.",
"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: {},
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
},
},
async () => {
async ({ workspace_id }) => {
try {
const [metadata, configuration, peerPage, sessionPage] = await Promise.all([
ctx.honcho.getMetadata(),
ctx.honcho.getConfiguration(),
ctx.honcho.peers(),
ctx.honcho.sessions(),
]);
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: ctx.honcho.workspaceId,
workspace_id: honcho.workspaceId,
metadata,
configuration,
peer_count: peerPage.total,
@ -48,24 +80,82 @@ export function register(server: McpServer, ctx: ToolContext) {
{
description: [
"List workspaces accessible to the current credentials (paginated).",
"Use this to discover available workspaces before selecting or switching context.",
"Returns workspace IDs with pagination metadata.",
"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: {},
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 () => {
async ({ page, size }) => {
try {
const page = await ctx.honcho.workspaces();
const result = await ctx.unscoped.http.post<
PageResponse<WorkspaceResponse>
>("/v3/workspaces/list", {
body: {},
query: { page, size },
});
return textResult({
workspaces: page.items.map((id) => ({ id })),
total: page.total,
page: page.page,
pages: page.pages,
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)}`,
return errorResult(withAdminKeyHint("Failed to list workspaces", 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 id = resolveWorkspaceId(ctx.config, workspace_id);
const workspace = await ctx.unscoped.http.post<WorkspaceResponse>(
"/v3/workspaces",
{
body: {
id,
metadata,
},
},
);
return textResult(formatWorkspace(workspace));
} catch (e) {
return errorResult(withAdminKeyHint("Failed to create workspace", e));
}
},
);
@ -84,6 +174,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"Returns {messages, conclusions}.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
query: z.string().describe("Search query."),
peer_id: z
.string()
@ -116,6 +207,7 @@ export function register(server: McpServer, ctx: ToolContext) {
},
},
async ({
workspace_id,
query,
peer_id,
session_id,
@ -125,7 +217,8 @@ export function register(server: McpServer, ctx: ToolContext) {
conclusion_filters,
}) => {
try {
const peer = peer_id ? await ctx.honcho.peer(peer_id) : null;
const honcho = ctx.clientFor(workspace_id);
const peer = peer_id ? await honcho.peer(peer_id) : null;
const messageOptions = {
filters: message_filters,
limit: message_limit,
@ -133,13 +226,13 @@ export function register(server: McpServer, ctx: ToolContext) {
const searchMessages = async () => {
if (session_id) {
const session = await ctx.honcho.session(session_id);
const session = await honcho.session(session_id);
return session.search(query, messageOptions);
}
if (peer) {
return peer.search(query, messageOptions);
}
return ctx.honcho.search(query, messageOptions);
return honcho.search(query, messageOptions);
};
// Conclusion search needs an (observer, observed) pair, so it only
@ -199,6 +292,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"- session_id only: get session metadata.",
].join("\n"),
inputSchema: {
workspace_id: workspaceIdSchema(ctx),
peer_id: z
.string()
.optional()
@ -209,17 +303,18 @@ export function register(server: McpServer, ctx: ToolContext) {
.describe("Optional: get metadata for this session."),
},
},
async ({ peer_id, session_id }) => {
async ({ workspace_id, peer_id, session_id }) => {
try {
const honcho = ctx.clientFor(workspace_id);
let metadata;
if (session_id) {
const session = await ctx.honcho.session(session_id);
const session = await honcho.session(session_id);
metadata = await session.getMetadata();
} else if (peer_id) {
const peer = await ctx.honcho.peer(peer_id);
const peer = await honcho.peer(peer_id);
metadata = await peer.getMetadata();
} else {
metadata = await ctx.honcho.getMetadata();
metadata = await honcho.getMetadata();
}
return textResult(metadata);
} catch (e) {
@ -242,6 +337,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"- 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."),
@ -255,18 +351,19 @@ export function register(server: McpServer, ctx: ToolContext) {
.describe("Optional: set metadata for this session."),
},
},
async ({ metadata, peer_id, session_id }) => {
async ({ workspace_id, metadata, peer_id, session_id }) => {
try {
const honcho = ctx.clientFor(workspace_id);
if (session_id) {
const session = await ctx.honcho.session(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 ctx.honcho.peer(peer_id);
const peer = await honcho.peer(peer_id);
await peer.setMetadata(metadata);
return textResult("Peer metadata set successfully");
} else {
await ctx.honcho.setMetadata(metadata);
await honcho.setMetadata(metadata);
return textResult("Workspace metadata set successfully");
}
} catch (e) {

View File

@ -1,10 +1,27 @@
import { z } from "zod";
import type { Honcho, Message, Summary, SessionSummaries } from "@honcho-ai/sdk";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { HonchoConfig } from "./config.js";
export interface ToolContext {
honcho: Honcho;
config: HonchoConfig;
/** Return a Honcho client scoped to the given workspace (or the header default). */
clientFor: (workspaceId?: string) => Honcho;
/** Client used only for credential-scoped ops (list workspaces). */
unscoped: Honcho;
}
/**
* Always optional at the schema layer so a missing value reaches clientFor,
* which returns a clear error (header or workspace_id on the next call).
*/
export function workspaceIdSchema(ctx: ToolContext) {
const fromHeader = ctx.config.workspaceId;
const description = fromHeader
? `Workspace to operate in. The connection already set X-Honcho-Workspace-ID=${fromHeader}; omit this argument unless you need a different workspace.`
: "Workspace to operate in. Prefer the client setting X-Honcho-Workspace-ID on the connection — then you can omit this on every call. Only pass it (or use list_workspaces / create_workspace) when the header is unset.";
const field = z.string().optional().describe(description);
return fromHeader ? field.default(fromHeader) : field;
}
export function textResult(

View File

@ -60,7 +60,7 @@ If you're unsure, list your available tools and look for Honcho memory tools (an
You need a Honcho API key — get one free at <https://app.honcho.dev> (starts with `hch-`). Then connect via the path you picked above — a purpose-built integration (recommended), or a raw connection:
- **MCP** — point your client at `https://mcp.honcho.dev` with two headers: `Authorization: Bearer hch-your-key-here` and `X-Honcho-User-Name: YourName` (what Honcho should call the user). Optional: `X-Honcho-Assistant-Name` (default `Assistant`) and `X-Honcho-Workspace-ID` (default `default`; set it to isolate memory per project). Restart the client fully after adding config. Per-client config snippets (Claude Desktop, Cursor, Codex, Windsurf, VS Code, Cline, Zed) are in the [MCP integration guide](https://honcho.dev/docs/v3/guides/integrations/mcp.md). Once connected, the server tells your assistant how to use the tools automatically.
- **MCP** — point your client at `https://mcp.honcho.dev` with `Authorization: Bearer hch-your-key-here`. Optional `X-Honcho-Workspace-ID` fills the `workspace_id` tool argument when omitted; otherwise pass `workspace_id` on each call (use `list_workspaces` to discover IDs). Restart the client fully after adding config. Per-client config snippets (Claude Desktop, Cursor, Codex, Windsurf, VS Code, Cline, Zed) are in the [MCP integration guide](https://honcho.dev/docs/v3/guides/integrations/mcp.md). Once connected, the server tells your assistant how to use the tools automatically.
- **CLI** — use the `honcho-cli` skill.
---