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

This commit is contained in:
ajspig 2026-08-13 14:28:53 -04:00
parent 0208f0dd2b
commit a66a09af54
3 changed files with 28 additions and 14 deletions

View File

@ -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;
}

View File

@ -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<WorkspaceResponse>(
"/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));
}
},
);

View File

@ -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(