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

This commit is contained in:
ajspig 2026-08-13 12:49:43 -04:00
parent 252269e9b6
commit 1e6539fa60
14 changed files with 335 additions and 146 deletions

View File

@ -344,7 +344,7 @@ Navigate to Claude Desktop's custom MCP servers settings and add Honcho:
}
```
**Optional customization** — You can also set a custom assistant name and workspace ID:
**Optional customization** — You can also set a custom assistant name:
```json
{
@ -359,21 +359,20 @@ Navigate to Claude Desktop's custom MCP servers settings and add Honcho:
"--header",
"X-Honcho-User-Name:${USER_NAME}",
"--header",
"X-Honcho-Assistant-Name:${ASSISTANT_NAME}",
"--header",
"X-Honcho-Workspace-ID:${WORKSPACE_ID}"
"X-Honcho-Assistant-Name:${ASSISTANT_NAME}"
],
"env": {
"AUTH_HEADER": "Bearer <your-honcho-key>",
"USER_NAME": "<your-name>",
"ASSISTANT_NAME": "<your-assistant-name>",
"WORKSPACE_ID": "<your-custom-workspace-id>"
"ASSISTANT_NAME": "<your-assistant-name>"
}
}
}
}
```
Workspace is selected per tool via `workspace_id`. You can also set `X-Honcho-Workspace-ID` on the connection to fill that argument by default. Use `list_workspaces` to discover IDs.
### Step 3: Restart Claude Desktop
Upon relaunch, Honcho should start and the tools will be available.

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,59 @@ 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 function resolveWorkspaceId(
config: HonchoConfig,
workspaceId?: string,
): string {
const id = workspaceId?.trim() || config.workspaceId?.trim();
if (!id) {
throw new Error(
"workspace_id is required. Pass it as a tool argument or set the X-Honcho-Workspace-ID header.",
);
}
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
@ -73,9 +75,9 @@ export function register(server: McpServer, ctx: ToolContext) {
.describe("Max results to return."),
},
},
async ({ peer_id, query, target_peer_id, top_k }) => {
async ({ workspace_id, peer_id, query, target_peer_id, top_k }) => {
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;
@ -108,6 +110,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()
@ -123,9 +126,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,
@ -152,6 +161,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()
@ -159,9 +169,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,7 +1,21 @@
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 } 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 ───────────────────────────────────────────────
@ -9,23 +23,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,
@ -47,19 +65,39 @@ 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(
@ -69,6 +107,47 @@ export function register(server: McpServer, ctx: ToolContext) {
},
);
// ── 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",
@ -81,6 +160,7 @@ export function register(server: McpServer, ctx: ToolContext) {
"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()
@ -92,17 +172,18 @@ export function register(server: McpServer, ctx: ToolContext) {
.describe("Optional: scope search to messages in this session."),
},
},
async ({ query, peer_id, session_id }) => {
async ({ workspace_id, query, peer_id, session_id }) => {
try {
const honcho = ctx.clientFor(workspace_id);
let messages;
if (session_id) {
const session = await ctx.honcho.session(session_id);
const session = await honcho.session(session_id);
messages = await session.search(query);
} else if (peer_id) {
const peer = await ctx.honcho.peer(peer_id);
const peer = await honcho.peer(peer_id);
messages = await peer.search(query);
} else {
messages = await ctx.honcho.search(query);
messages = await honcho.search(query);
}
return textResult(formatMessages(messages));
} catch (e) {
@ -124,6 +205,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()
@ -134,17 +216,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) {
@ -167,6 +250,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."),
@ -180,18 +264,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,24 @@
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;
}
/** Required unless X-Honcho-Workspace-ID is set, in which case that value is the default. */
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 base = z.string().min(1).describe(description);
return fromHeader ? base.optional().default(fromHeader) : base;
}
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.
---