From 29b6554cb20f07dfb5879246ccd1b03c181fefde Mon Sep 17 00:00:00 2001 From: mantrakp04 Date: Tue, 16 Jun 2026 12:44:48 -0700 Subject: [PATCH] Add skill context to Ask Hexclave --- .../app/api/latest/ai/query/[mode]/route.ts | 2 + .../src/lib/ai/mcp-skill-context.test.ts | 51 +++++++++++++++++++ apps/backend/src/lib/ai/mcp-skill-context.ts | 30 +++++++++++ 3 files changed, 83 insertions(+) create mode 100644 apps/backend/src/lib/ai/mcp-skill-context.test.ts create mode 100644 apps/backend/src/lib/ai/mcp-skill-context.ts diff --git a/apps/backend/src/app/api/latest/ai/query/[mode]/route.ts b/apps/backend/src/app/api/latest/ai/query/[mode]/route.ts index 093358b8c..da68e5b23 100644 --- a/apps/backend/src/app/api/latest/ai/query/[mode]/route.ts +++ b/apps/backend/src/app/api/latest/ai/query/[mode]/route.ts @@ -3,6 +3,7 @@ import { selectModel } from "@/lib/ai/models"; import { getFullSystemPrompt } from "@/lib/ai/prompts"; import { reviewMcpCall } from "@/lib/ai/qa-reviewer"; import { requestBodySchema } from "@/lib/ai/schema"; +import { getMcpSkillContextPrompt } from "@/lib/ai/mcp-skill-context"; import { getTools } from "@/lib/ai/tools"; import { getVerifiedQaContext } from "@/lib/ai/verified-qa"; import { listManagedProjectIds } from "@/lib/projects"; @@ -61,6 +62,7 @@ export const POST = createSmartRouteHandler({ if (isDocsOrSearch) { systemPrompt += await getVerifiedQaContext(); } + systemPrompt += await getMcpSkillContextPrompt(body.mcpCallMetadata?.toolName); const tools = await getTools(toolNames, { auth: fullReq.auth, targetProjectId: projectId }); const toolsArg = Object.keys(tools).length > 0 ? tools : undefined; const isCreateDashboard = systemPromptId === "create-dashboard"; diff --git a/apps/backend/src/lib/ai/mcp-skill-context.test.ts b/apps/backend/src/lib/ai/mcp-skill-context.test.ts new file mode 100644 index 000000000..c6df7ce4c --- /dev/null +++ b/apps/backend/src/lib/ai/mcp-skill-context.test.ts @@ -0,0 +1,51 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { getMcpSkillContextPrompt } from "./mcp-skill-context"; + +describe("getMcpSkillContextPrompt", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("does not fetch skill context for non-ask_hexclave requests", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch"); + + await expect(getMcpSkillContextPrompt("other_tool")).resolves.toMatchInlineSnapshot(`""`); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("fetches and embeds the canonical skill for ask_hexclave requests", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("# Hexclave Skill\n\nUse Hexclave docs."), + ); + + await expect(getMcpSkillContextPrompt("ask_hexclave")).resolves.toMatchInlineSnapshot(` + " + + ## MCP-Provided Hexclave Skill Context + + The current request came through the public Hexclave MCP server's ask_hexclave tool. + The backend fetched the canonical Hexclave agent skill from https://skill.hexclave.com + immediately before spawning this assistant. Treat this skill content as baseline context + for answering the user's question, while still using documentation tools for specific + facts and citations: + + # Hexclave Skill + + Use Hexclave docs. + " + `); + expect(fetchSpy).toHaveBeenCalledWith("https://skill.hexclave.com", { + headers: { Accept: "text/markdown" }, + }); + }); + + it("fails loudly when the canonical skill cannot be fetched", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("missing", { status: 503, statusText: "Service Unavailable" }), + ); + + await expect(getMcpSkillContextPrompt("ask_hexclave")).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Failed to fetch skill from https://skill.hexclave.com: 503 Service Unavailable]`, + ); + }); +}); diff --git a/apps/backend/src/lib/ai/mcp-skill-context.ts b/apps/backend/src/lib/ai/mcp-skill-context.ts new file mode 100644 index 000000000..e3af3433a --- /dev/null +++ b/apps/backend/src/lib/ai/mcp-skill-context.ts @@ -0,0 +1,30 @@ +const hexclaveSkillResourceUri = "https://skill.hexclave.com"; + +export async function getMcpSkillContextPrompt(toolName: string | null | undefined): Promise { + if (toolName !== "ask_hexclave") { + return ""; + } + + const response = await fetch(hexclaveSkillResourceUri, { + headers: { Accept: "text/markdown" }, + }); + if (!response.ok) { + throw new Error( + `Failed to fetch skill from ${hexclaveSkillResourceUri}: ${response.status} ${response.statusText}`, + ); + } + + const skillContext = await response.text(); + return ` + +## MCP-Provided Hexclave Skill Context + +The current request came through the public Hexclave MCP server's ask_hexclave tool. +The backend fetched the canonical Hexclave agent skill from https://skill.hexclave.com +immediately before spawning this assistant. Treat this skill content as baseline context +for answering the user's question, while still using documentation tools for specific +facts and citations: + +${skillContext} +`; +}