From a66a09af54d09452b6074d5d4faf611eab76617b Mon Sep 17 00:00:00 2001 From: ajspig Date: Thu, 13 Aug 2026 14:28:53 -0400 Subject: [PATCH] fix(mcp): clarify missing-workspace and admin-key errors --- mcp/src/config.ts | 7 ++++--- mcp/src/tools/workspace.ts | 26 ++++++++++++++++++-------- mcp/src/types.ts | 9 ++++++--- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/mcp/src/config.ts b/mcp/src/config.ts index f88d627a..34484822 100644 --- a/mcp/src/config.ts +++ b/mcp/src/config.ts @@ -47,15 +47,16 @@ export function parseConfig(request: Request, env: Env = {}): HonchoConfig { }; } +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( - "workspace_id is required. Pass it as a tool argument or set the X-Honcho-Workspace-ID header.", - ); + throw new Error(MISSING_WORKSPACE_ID_MESSAGE); } return id; } diff --git a/mcp/src/tools/workspace.ts b/mcp/src/tools/workspace.ts index 3e162646..2d626989 100644 --- a/mcp/src/tools/workspace.ts +++ b/mcp/src/tools/workspace.ts @@ -1,7 +1,12 @@ import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { PageResponse, WorkspaceResponse } from "@honcho-ai/sdk"; +import { + HonchoError, + type PageResponse, + type WorkspaceResponse, +} from "@honcho-ai/sdk"; import type { ToolContext } from "../types.js"; +import { resolveWorkspaceId } from "../config.js"; import { textResult, errorResult, @@ -9,6 +14,14 @@ import { 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, @@ -100,9 +113,7 @@ export function register(server: McpServer, ctx: ToolContext) { 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)); } }, ); @@ -130,20 +141,19 @@ export function register(server: McpServer, ctx: ToolContext) { }, async ({ workspace_id, metadata }) => { try { + const id = resolveWorkspaceId(ctx.config, workspace_id); const workspace = await ctx.unscoped.http.post( "/v3/workspaces", { body: { - id: workspace_id, + id, metadata, }, }, ); return textResult(formatWorkspace(workspace)); } catch (e) { - return errorResult( - `Failed to create workspace: ${e instanceof Error ? e.message : String(e)}`, - ); + return errorResult(withAdminKeyHint("Failed to create workspace", e)); } }, ); diff --git a/mcp/src/types.ts b/mcp/src/types.ts index a67229fd..15a8d870 100644 --- a/mcp/src/types.ts +++ b/mcp/src/types.ts @@ -11,14 +11,17 @@ export interface ToolContext { unscoped: Honcho; } -/** Required unless X-Honcho-Workspace-ID is set, in which case that value is the default. */ +/** + * 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 base = z.string().min(1).describe(description); - return fromHeader ? base.optional().default(fromHeader) : base; + const field = z.string().optional().describe(description); + return fromHeader ? field.default(fromHeader) : field; } export function textResult(