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:
parent
b7d4a2a3ad
commit
e82d2ef497
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`) |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
|
|
|||
Loading…
Reference in New Issue