diff --git a/.github/workflows/deploy-workers.yml b/.github/workflows/deploy-workers.yml
index ee9eb0e5..abc262ad 100644
--- a/.github/workflows/deploy-workers.yml
+++ b/.github/workflows/deploy-workers.yml
@@ -51,6 +51,12 @@ jobs:
- name: Configure Cloudflare Worker Secrets (dev)
run: |
echo "$SANITY_API_READ_TOKEN" | pnpm --filter @codingcatdev/site exec wrangler secret put SANITY_API_READ_TOKEN --env=""
+ if [ -n "$SANITY_AUTH_TOKEN" ]; then
+ echo "$SANITY_AUTH_TOKEN" | pnpm --filter @codingcatdev/site exec wrangler secret put SANITY_AUTH_TOKEN --env=""
+ fi
+ if [ -n "$SANITY_CONTEXT_TOKEN" ]; then
+ echo "$SANITY_CONTEXT_TOKEN" | pnpm --filter @codingcatdev/site exec wrangler secret put SANITY_CONTEXT_TOKEN --env=""
+ fi
if [ -n "$OPENROUTER_API_KEY" ]; then
echo "$OPENROUTER_API_KEY" | pnpm --filter @codingcatdev/site exec wrangler secret put OPENROUTER_API_KEY --env=""
fi
@@ -58,6 +64,8 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
SANITY_API_READ_TOKEN: ${{ secrets.SANITY_API_READ_TOKEN }}
+ SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
+ SANITY_CONTEXT_TOKEN: ${{ secrets.SANITY_CONTEXT_TOKEN }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
deploy-production:
@@ -97,6 +105,12 @@ jobs:
- name: Configure Cloudflare Worker Secrets (production)
run: |
echo "$SANITY_API_READ_TOKEN" | pnpm --filter @codingcatdev/site exec wrangler secret put SANITY_API_READ_TOKEN --env production
+ if [ -n "$SANITY_AUTH_TOKEN" ]; then
+ echo "$SANITY_AUTH_TOKEN" | pnpm --filter @codingcatdev/site exec wrangler secret put SANITY_AUTH_TOKEN --env production
+ fi
+ if [ -n "$SANITY_CONTEXT_TOKEN" ]; then
+ echo "$SANITY_CONTEXT_TOKEN" | pnpm --filter @codingcatdev/site exec wrangler secret put SANITY_CONTEXT_TOKEN --env production
+ fi
if [ -n "$OPENROUTER_API_KEY" ]; then
echo "$OPENROUTER_API_KEY" | pnpm --filter @codingcatdev/site exec wrangler secret put OPENROUTER_API_KEY --env production
fi
@@ -104,4 +118,6 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
SANITY_API_READ_TOKEN: ${{ secrets.SANITY_API_READ_TOKEN }}
+ SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
+ SANITY_CONTEXT_TOKEN: ${{ secrets.SANITY_CONTEXT_TOKEN }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
diff --git a/apps/site/.dev.vars.example b/apps/site/.dev.vars.example
index 7f2b277f..94e4b63e 100644
--- a/apps/site/.dev.vars.example
+++ b/apps/site/.dev.vars.example
@@ -1,5 +1,8 @@
# Local secrets for `wrangler dev` / platformProxy. Copy to .dev.vars (gitignored).
SANITY_API_READ_TOKEN=
+# Optional: Organization-level Sanity Context Viewer token for https://api.sanity.io/v1/context/organizations/ovF2qiKSO/mcp/agent-aj-mcp
+SANITY_CONTEXT_TOKEN=
+SANITY_AUTH_TOKEN=
# Optional: PostHog project key for visitor flows, funnels, and LMS telemetry via /ingest proxy
PUBLIC_POSTHOG_KEY=
PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
diff --git a/apps/site/src/components/ChatModal.astro b/apps/site/src/components/ChatModal.astro
index 00504657..75c4d902 100644
--- a/apps/site/src/components/ChatModal.astro
+++ b/apps/site/src/components/ChatModal.astro
@@ -96,6 +96,19 @@
Top videos
+
@@ -139,6 +152,19 @@
Top videos
+
diff --git a/apps/site/src/pages/.well-known/mcp/server-card.json.ts b/apps/site/src/pages/.well-known/mcp/server-card.json.ts
index 94bc7eb9..f71cfce2 100644
--- a/apps/site/src/pages/.well-known/mcp/server-card.json.ts
+++ b/apps/site/src/pages/.well-known/mcp/server-card.json.ts
@@ -192,6 +192,28 @@ export const GET: APIRoute = async ({ locals }) => {
required: ["mode", "documentId", "field"],
},
},
+ {
+ name: "knowledge_base_read",
+ title: "Read Knowledge Base Entries",
+ description:
+ "Read full entries from the CodingCat.dev Sanity Context Knowledge Base (e.g. 'AJ Knowlege Jokes' — programming & tech dad jokes, Git jokes, SQL jokes, Jeff Dean facts, coding culture).",
+ inputSchema: {
+ type: "object",
+ properties: {
+ knowledgeBaseId: {
+ type: "string",
+ description: "The ID of the Knowledge Base to read from.",
+ },
+ paths: {
+ type: "array",
+ items: { type: "string" },
+ description:
+ "List of entry paths to read from the Knowledge Base (up to 20 paths).",
+ },
+ },
+ required: ["knowledgeBaseId", "paths"],
+ },
+ },
],
};
diff --git a/apps/site/src/pages/api/chat.ts b/apps/site/src/pages/api/chat.ts
index 3a6e3d31..1769c76a 100644
--- a/apps/site/src/pages/api/chat.ts
+++ b/apps/site/src/pages/api/chat.ts
@@ -26,13 +26,144 @@ export const OPTIONS: APIRoute = async () => {
});
};
-export const GET: APIRoute = async () => {
+function getCandidateMcpTokens(
+ cfEnv: Record | undefined,
+): string[] {
+ return Array.from(
+ new Set(
+ [
+ cfEnv?.SANITY_CONTEXT_TOKEN as string | undefined,
+ process.env.SANITY_CONTEXT_TOKEN,
+ cfEnv?.SANITY_AUTH_TOKEN as string | undefined,
+ process.env.SANITY_AUTH_TOKEN,
+ cfEnv?.SANITY_API_READ_TOKEN as string | undefined,
+ process.env.SANITY_API_READ_TOKEN,
+ cfEnv?.SANITY_API_WRITE_TOKEN as string | undefined,
+ process.env.SANITY_API_WRITE_TOKEN,
+ ]
+ .map((t) => (typeof t === "string" ? t.trim() : ""))
+ .filter(Boolean),
+ ),
+ );
+}
+
+function extractMcpText(mcpResult: unknown): string {
+ if (!mcpResult) {
+ return "";
+ }
+ if (typeof mcpResult === "string") {
+ return mcpResult.trim();
+ }
+ if (typeof mcpResult === "object") {
+ const obj = mcpResult as Record;
+ if (Array.isArray(obj.content)) {
+ const texts = obj.content
+ .map((part: any) =>
+ part && typeof part === "object" && typeof part.text === "string"
+ ? part.text
+ : "",
+ )
+ .filter(Boolean);
+ if (texts.length > 0) {
+ return texts.join("\n\n").trim();
+ }
+ }
+ if (typeof obj.text === "string") {
+ return obj.text.trim();
+ }
+ if (obj.result !== undefined) {
+ return typeof obj.result === "string"
+ ? obj.result.trim()
+ : JSON.stringify(obj.result, null, 2);
+ }
+ }
+ return "";
+}
+
+async function connectSanityContextMcp(
+ cfEnv: Record | undefined,
+): Promise<{
+ client?: Awaited>;
+ tools: Record;
+ error?: string;
+}> {
+ const tokens = getCandidateMcpTokens(cfEnv);
+ if (tokens.length === 0) {
+ return {
+ tools: {},
+ error:
+ "No Sanity API/Context token configured (SANITY_CONTEXT_TOKEN, SANITY_AUTH_TOKEN, or SANITY_API_READ_TOKEN).",
+ };
+ }
+
+ let lastError = "";
+ for (const token of tokens) {
+ let client: Awaited> | undefined;
+ try {
+ client = await createMCPClient({
+ transport: {
+ type: "http",
+ url: SANITY_CONTEXT_MCP_URL,
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ redirect: "follow",
+ fetch: (input, init) => {
+ const sanitizedInit = init ? { ...init } : {};
+ if (sanitizedInit.redirect === "error") {
+ sanitizedInit.redirect = "follow";
+ }
+ return globalThis.fetch(input, sanitizedInit);
+ },
+ },
+ });
+ const tools = await client.tools();
+ return { client, tools: tools || {} };
+ } catch (err) {
+ lastError = err instanceof Error ? err.message : String(err);
+ if (client) {
+ try {
+ await client.close();
+ } catch {}
+ }
+ }
+ }
+
+ console.warn(
+ "[Chat API MCP connection warning, using direct Sanity client]",
+ lastError,
+ );
+ return { tools: {}, error: lastError };
+}
+
+export const GET: APIRoute = async ({ request }) => {
+ const url = new URL(request.url);
+ const verify = url.searchParams.get("verify") === "1";
+ const cfEnv = env as unknown as Record | undefined;
+
+ let mcpDiagnostic: Record = {};
+ if (verify) {
+ const { client, tools, error } = await connectSanityContextMcp(cfEnv);
+ const toolNames = Object.keys(tools);
+ mcpDiagnostic = {
+ mcpConnected: toolNames.length > 0,
+ mcpTools: toolNames,
+ ...(error ? { mcpError: error } : {}),
+ };
+ if (client) {
+ try {
+ await client.close();
+ } catch {}
+ }
+ }
+
return new Response(
JSON.stringify({
status: "ready",
name: "Agent AJ",
model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
mcp: SANITY_CONTEXT_MCP_URL,
+ ...mcpDiagnostic,
}),
{
status: 200,
@@ -48,7 +179,9 @@ export const POST: APIRoute = async ({ request }) => {
const cfEnv = env as unknown as Record | undefined;
const sanityToken =
(cfEnv?.SANITY_API_READ_TOKEN as string | undefined) ||
- process.env.SANITY_API_READ_TOKEN;
+ (cfEnv?.SANITY_AUTH_TOKEN as string | undefined) ||
+ process.env.SANITY_API_READ_TOKEN ||
+ process.env.SANITY_AUTH_TOKEN;
let body: {
messages?: Array<{ role: string; content: string }>;
@@ -97,35 +230,54 @@ export const POST: APIRoute = async ({ request }) => {
let mcpClient: Awaited> | undefined;
try {
- let mcpGroqTool: any;
- if (sanityToken) {
+ const collectedMcpOutputs: Array<{ toolName: string; text: string }> = [];
+ const { client: connectedMcpClient, tools: rawMcpTools } =
+ await connectSanityContextMcp(cfEnv);
+ mcpClient = connectedMcpClient;
+
+ const mcpGroqTool = rawMcpTools.groq_query;
+ let initialContextOutline = "";
+ let cachedInitialContextResult: unknown;
+
+ // Pre-fetch initial_context from the Sanity Context MCP endpoint if available
+ // so Agent AJ immediately knows the Knowledge Base IDs, entry paths, and instructions on Step 1.
+ if (typeof rawMcpTools.initial_context?.execute === "function") {
try {
- // 1. Optionally connect MCP client to Sanity Context MCP endpoint
- mcpClient = await createMCPClient({
- transport: {
- type: "http",
- url: SANITY_CONTEXT_MCP_URL,
- headers: {
- Authorization: `Bearer ${sanityToken}`,
- },
- redirect: "follow",
- fetch: (input, init) => {
- const sanitizedInit = init ? { ...init } : {};
- if (sanitizedInit.redirect === "error") {
- sanitizedInit.redirect = "follow";
- }
- return globalThis.fetch(input, sanitizedInit);
- },
- },
- });
-
- const mcpTools = await mcpClient.tools();
- mcpGroqTool = mcpTools.groq_query;
- } catch (mcpErr) {
- console.warn(
- "[Chat API MCP connection warning, using direct Sanity client]",
- mcpErr,
+ cachedInitialContextResult = await rawMcpTools.initial_context.execute(
+ {},
+ {} as any,
);
+ initialContextOutline = extractMcpText(cachedInitialContextResult);
+ } catch (initCtxErr) {
+ console.warn("[Chat API initial_context prefetch warning]", initCtxErr);
+ }
+ }
+
+ // Wrap all MCP tools so they are exposed to streamText AND their outputs are captured for fallback synthesis
+ const wrappedMcpTools: Record = {};
+ for (const [toolName, mcpTool] of Object.entries(rawMcpTools)) {
+ if (!mcpTool || typeof mcpTool !== "object") {
+ continue;
+ }
+ const origExecute = (mcpTool as any).execute;
+ if (typeof origExecute === "function") {
+ wrappedMcpTools[toolName] = {
+ ...mcpTool,
+ execute: async (args: any, options: any) => {
+ const res =
+ toolName === "initial_context" &&
+ cachedInitialContextResult !== undefined
+ ? cachedInitialContextResult
+ : await origExecute(args, options);
+ const text = extractMcpText(res);
+ if (text && !(res as any)?.isError) {
+ collectedMcpOutputs.push({ toolName, text });
+ }
+ return res;
+ },
+ };
+ } else {
+ wrappedMcpTools[toolName] = mcpTool;
}
}
@@ -237,7 +389,8 @@ export const POST: APIRoute = async ({ request }) => {
return normalized;
};
- const tools = {
+ const tools: Record = {
+ ...wrappedMcpTools,
search_content: tool({
description:
"Search CodingCat.dev articles, podcast episodes, authors, and transcripts using hybrid semantic search and keyword ranking. ALWAYS use this tool first when asked to find, recommend, or query podcasts, articles, or topics (e.g. 'Sanity', 'Firebase', 'Next.js', 'AI', 'web dev').",
@@ -541,20 +694,26 @@ export const POST: APIRoute = async ({ request }) => {
}
// 3. Stream model response with multi-step tool calling
+ const systemPrompt =
+ "You are Agent AJ, the CodingCat.dev mascot and intelligent AI assistant. Answer user queries about CodingCat.dev podcasts, tutorials, articles, authors, transcripts, and our Sanity Context Knowledge Base (including AJ Knowledge Jokes).\n\n" +
+ "SEARCH & TOOL INSTRUCTIONS:\n" +
+ "- KNOWLEDGE BASE & JOKES (`knowledge_base_read` / `initial_context`): When asked for a joke, programming dad joke, Git joke, SQL/database joke, Jeff Dean fact, legendary software engineering story, or coding culture humor, ALWAYS use the `knowledge_base_read` tool (and `initial_context` first if you need to see the knowledge base ID and entry paths) to read the actual entries from the Sanity Context Knowledge Base (`AJ Knowlege Jokes`) and share the real jokes/entries retrieved from it!\n" +
+ "- RECENT CONTENT (`get_recent_content`): ALWAYS use the `get_recent_content` tool first when asked for 'latest podcast', 'recent episodes', 'latest blog post', or 'new articles'.\n" +
+ "- TOPIC SEARCH (`search_content`): ALWAYS use the `search_content` tool when asked to find, recommend, or query podcasts, posts, or articles on any topic, technology, or question (e.g. 'Sanity', 'Firebase', 'Next.js', 'AI', 'web dev'). It performs hybrid semantic and keyword search across all episodes, posts, and transcripts.\n" +
+ "- TOP VIDEOS: When asked for 'top videos' or popular video podcasts, search for high-profile episodes with video recordings (e.g. episodes featuring Guillermo Rauch, Rich Harris, Lee Robinson, or recent GenAI/MCP video podcasts) and provide their YouTube links alongside CodingCat.dev episode links.\n" +
+ "- PRECISE GROQ (`groq_query`): Use `groq_query` only for precise lookups when searching for specific author profiles (e.g. *[_type == 'author' && title match '*Alex*']).\n" +
+ "- CRITICAL: ALWAYS execute tools by invoking them via function calls. NEVER output raw JSON or code blocks in your text describing tool calls instead of executing them.\n" +
+ '- Once you have retrieved the final content (either from `knowledge_base_read` for knowledge base entries or from `search_content`/`get_recent_content` for episodes and posts), synthesize it into an engaging, helpful response. For podcasts and posts, list the relevant titles, brief descriptions, and markdown links using relative paths (e.g. [Episode Title](/podcast/slug) or [Post Title](/post/slug)). When an exact YouTube URL is present in the tool results (e.g. item.youtube), include that exact URL on its own line (e.g. https://www.youtube.com/watch?v=VIDEO_ID or https://youtu.be/VIDEO_ID). NEVER invent placeholder strings like "https://www.youtube.com/watch?v=VIDEO_ID", localhost URLs, or malformed URL brackets like ["/slug"](http://localhost:3000/slug).' +
+ (initialContextOutline
+ ? `\n\nSANITY CONTEXT MCP INITIAL CONTEXT OUTLINE:\n${initialContextOutline}`
+ : "");
+
const result = streamText({
model,
- system:
- "You are Agent AJ, CodingCat.dev's intelligent AI assistant. Answer user queries about CodingCat.dev podcasts, tutorials, articles, authors, and transcripts.\n\n" +
- "SEARCH & TOOL INSTRUCTIONS:\n" +
- "- ALWAYS use the `get_recent_content` tool first when asked for 'latest podcast', 'recent episodes', 'latest blog post', or 'new articles'.\n" +
- "- ALWAYS use the `search_content` tool when asked to find, recommend, or query podcasts, posts, or articles on any topic, technology, or question (e.g. 'Sanity', 'Firebase', 'Next.js', 'AI', 'web dev'). It performs hybrid semantic and keyword search across all episodes, posts, and transcripts.\n" +
- "- When asked for 'top videos' or popular video podcasts, search for high-profile episodes with video recordings (e.g. episodes featuring Guillermo Rauch, Rich Harris, Lee Robinson, or recent GenAI/MCP video podcasts) and provide their YouTube links alongside CodingCat.dev episode links.\n" +
- "- Use `groq_query` only for precise lookups when searching for specific author profiles (e.g. *[_type == 'author' && title match '*Alex*']).\n" +
- "- CRITICAL: ALWAYS execute tools by invoking them via function calls. NEVER output raw JSON or code blocks in your text describing tool calls instead of executing them.\n" +
- '- Once you receive results from your tool calls, immediately synthesize them into an engaging, helpful response without calling more tools. List the relevant episode or article titles, brief descriptions, and markdown links using relative paths (e.g. [Episode Title](/podcast/slug) or [Post Title](/post/slug)). When an exact YouTube URL is present in the tool results (e.g. item.youtube), include that exact URL on its own line (e.g. https://www.youtube.com/watch?v=VIDEO_ID or https://youtu.be/VIDEO_ID). NEVER invent placeholder strings like "https://www.youtube.com/watch?v=VIDEO_ID", localhost URLs, or malformed URL brackets like ["/slug"](http://localhost:3000/slug).',
+ system: systemPrompt,
messages: coreMessages,
tools,
- stopWhen: isStepCount(5),
+ stopWhen: isStepCount(8),
onError: ({ error }) => {
console.error("[StreamText Internal Error]", error);
},
@@ -598,6 +757,52 @@ export const POST: APIRoute = async ({ request }) => {
.join("\n\n");
if (allText) {
await writer.write(encoder.encode(allText));
+ } else if (collectedMcpOutputs.length > 0) {
+ console.warn(
+ "[Chat API Synthesizing Fallback from MCP Tool Results]",
+ {
+ stepCount: steps.length,
+ mcpToolsCalled: collectedMcpOutputs.map((o) => o.toolName),
+ },
+ );
+ // Prefer non-initial_context outputs (e.g. knowledge_base_read) when available
+ const primaryMcpOutputs = collectedMcpOutputs.filter(
+ (o) => o.toolName !== "initial_context",
+ );
+ const mcpContextText = (
+ primaryMcpOutputs.length > 0
+ ? primaryMcpOutputs
+ : collectedMcpOutputs
+ )
+ .map((o) => o.text)
+ .join("\n\n");
+
+ let synthesized = false;
+ try {
+ const synthResult = streamText({
+ model,
+ system:
+ "You are Agent AJ, the CodingCat.dev mascot. Answer the user's request directly and enthusiastically using the retrieved Sanity Context Knowledge Base content below.",
+ messages: [
+ ...coreMessages,
+ {
+ role: "user",
+ content: `Retrieved Knowledge Base Content:\n\n${mcpContextText}\n\nPlease answer the user's request based on this content.`,
+ },
+ ],
+ });
+ for await (const part of synthResult.stream) {
+ if (part.type === "text-delta" && part.text) {
+ synthesized = true;
+ await writer.write(encoder.encode(part.text));
+ }
+ }
+ } catch (synthErr) {
+ console.warn("[Chat API MCP synthesis error]", synthErr);
+ }
+ if (!synthesized) {
+ await writer.write(encoder.encode(mcpContextText));
+ }
} else if (collectedToolItems.length > 0) {
console.warn("[Chat API Synthesizing Fallback from Tool Results]", {
stepCount: steps.length,
diff --git a/apps/site/src/pages/api/mcp.ts b/apps/site/src/pages/api/mcp.ts
index 45909c86..43a83da1 100644
--- a/apps/site/src/pages/api/mcp.ts
+++ b/apps/site/src/pages/api/mcp.ts
@@ -183,13 +183,135 @@ const MCP_TOOLS = [
required: ["mode", "documentId", "field"],
},
},
+ {
+ name: "knowledge_base_read",
+ title: "Read Knowledge Base Entries",
+ description:
+ "Read full entries from the CodingCat.dev Sanity Context Knowledge Base (e.g. 'AJ Knowlege Jokes' — programming & tech dad jokes, Git jokes, SQL jokes, Jeff Dean facts, coding culture). Call initial_context first if you need the knowledgeBaseId and entry paths.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ knowledgeBaseId: {
+ type: "string",
+ description: "The ID of the Knowledge Base to read from.",
+ },
+ paths: {
+ type: "array",
+ items: { type: "string" },
+ description:
+ "List of entry paths to read from the Knowledge Base (up to 20 paths).",
+ },
+ },
+ required: ["knowledgeBaseId", "paths"],
+ },
+ },
];
+const SANITY_CONTEXT_MCP_URL =
+ "https://api.sanity.io/v1/context/organizations/ovF2qiKSO/mcp/agent-aj-mcp";
+
+function getCandidateMcpTokens(): string[] {
+ const cfEnv = env as unknown as Record | undefined;
+ return Array.from(
+ new Set(
+ [
+ cfEnv?.SANITY_CONTEXT_TOKEN as string | undefined,
+ process.env.SANITY_CONTEXT_TOKEN,
+ cfEnv?.SANITY_AUTH_TOKEN as string | undefined,
+ process.env.SANITY_AUTH_TOKEN,
+ cfEnv?.SANITY_API_READ_TOKEN as string | undefined,
+ process.env.SANITY_API_READ_TOKEN,
+ cfEnv?.SANITY_API_WRITE_TOKEN as string | undefined,
+ process.env.SANITY_API_WRITE_TOKEN,
+ ]
+ .map((t) => (typeof t === "string" ? t.trim() : ""))
+ .filter(Boolean),
+ ),
+ );
+}
+
+async function callSanityContextMcpTool(
+ toolName: string,
+ toolArgs: Record,
+): Promise<{
+ content: Array<{ type: string; text: string }>;
+ isError?: boolean;
+} | null> {
+ const tokens = getCandidateMcpTokens();
+ for (const token of tokens) {
+ try {
+ const headers: Record = {
+ "Content-Type": "application/json",
+ Accept: "application/json, text/event-stream",
+ Authorization: `Bearer ${token}`,
+ };
+ const initRes = await globalThis.fetch(SANITY_CONTEXT_MCP_URL, {
+ method: "POST",
+ headers,
+ body: JSON.stringify({
+ jsonrpc: "2.0",
+ id: 1,
+ method: "initialize",
+ params: {
+ protocolVersion: "2025-03-26",
+ capabilities: {},
+ clientInfo: MCP_SERVER_INFO,
+ },
+ }),
+ });
+ if (!initRes.ok) {
+ continue;
+ }
+ const sessionId = initRes.headers.get("mcp-session-id");
+ const callRes = await globalThis.fetch(SANITY_CONTEXT_MCP_URL, {
+ method: "POST",
+ headers: {
+ ...headers,
+ ...(sessionId ? { "mcp-session-id": sessionId } : {}),
+ },
+ body: JSON.stringify({
+ jsonrpc: "2.0",
+ id: 2,
+ method: "tools/call",
+ params: {
+ name: toolName,
+ arguments: toolArgs,
+ },
+ }),
+ });
+ if (!callRes.ok) {
+ continue;
+ }
+ const rawText = await callRes.text();
+ let parsed: any;
+ if (rawText.includes("data: ")) {
+ const dataLine = rawText
+ .split("\n")
+ .find((l) => l.startsWith("data: "));
+ if (dataLine) {
+ parsed = JSON.parse(dataLine.slice(6));
+ }
+ } else {
+ parsed = JSON.parse(rawText);
+ }
+ if (parsed?.result?.content && Array.isArray(parsed.result.content)) {
+ return {
+ content: parsed.result.content,
+ isError: Boolean(parsed.result.isError),
+ };
+ }
+ } catch {}
+ }
+ return null;
+}
+
function getSanityClient() {
const cfEnv = env as unknown as Record | undefined;
const token =
(cfEnv?.SANITY_API_READ_TOKEN as string | undefined) ||
- process.env.SANITY_API_READ_TOKEN;
+ (cfEnv?.SANITY_AUTH_TOKEN as string | undefined) ||
+ process.env.SANITY_API_READ_TOKEN ||
+ process.env.SANITY_AUTH_TOKEN;
return createClient({
projectId: (cfEnv?.PUBLIC_SANITY_PROJECT_ID as string) || "hfh83o0w",
@@ -687,6 +809,10 @@ async function executeTool(
case "arrayFieldReader":
return handleArrayFieldReader(client, args);
+ case "knowledge_base_read":
+ case "knowledgeBaseRead":
+ return handleKnowledgeBaseRead(args);
+
default:
return {
content: [
@@ -1070,17 +1196,25 @@ async function handleInitialContext(
client: ReturnType,
) {
try {
- const counts = await client.fetch(`{
- "podcasts": count(*[_type == "podcast"]),
- "posts": count(*[_type == "post"]),
- "authors": count(*[_type == "author"]),
- "guests": count(*[_type == "guest"]),
- "recentPodcasts": *[_type == "podcast" && defined(slug.current)] | order(date desc)[0...3]{
- title,
- "slug": slug.current,
- date
- }
- }`);
+ const [counts, sanityContextInitial] = await Promise.all([
+ client.fetch(`{
+ "podcasts": count(*[_type == "podcast"]),
+ "posts": count(*[_type == "post"]),
+ "authors": count(*[_type == "author"]),
+ "guests": count(*[_type == "guest"]),
+ "recentPodcasts": *[_type == "podcast" && defined(slug.current)] | order(date desc)[0...3]{
+ title,
+ "slug": slug.current,
+ date
+ }
+ }`),
+ callSanityContextMcpTool("initial_context", {}).catch(() => null),
+ ]);
+
+ const knowledgeBaseContext = sanityContextInitial?.content
+ ?.map((c) => c.text)
+ .filter(Boolean)
+ .join("\n\n");
const summary = {
platform: "CodingCat.dev",
@@ -1098,11 +1232,15 @@ async function handleInitialContext(
url: `https://codingcat.dev/podcast/${p.slug}`,
date: p.date,
})),
+ ...(knowledgeBaseContext
+ ? { sanityContextKnowledgeBase: knowledgeBaseContext }
+ : {}),
recommendedTools: [
"search_content: Fast semantic & keyword search across all episodes, tutorials, and transcripts.",
"get_content_by_slug: Retrieve full episode or post details, show notes, and transcript summaries.",
"get_recent_content: Get the newest podcasts and articles.",
"query_content: Run custom read-only GROQ queries.",
+ "knowledge_base_read: Read full entries from the Sanity Context Knowledge Base (e.g. 'AJ Knowlege Jokes').",
],
};
@@ -1124,6 +1262,7 @@ async function handleInitialContext(
"get_content_by_slug",
"get_recent_content",
"query_content",
+ "knowledge_base_read",
],
}),
},
@@ -1133,6 +1272,26 @@ async function handleInitialContext(
}
}
+async function handleKnowledgeBaseRead(args: Record) {
+ const result = await callSanityContextMcpTool("knowledge_base_read", args);
+ if (result) {
+ return result;
+ }
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify({
+ error:
+ "Unable to reach Sanity Context Knowledge Base MCP endpoint or missing Organization Context Viewer token (SANITY_CONTEXT_TOKEN / SANITY_AUTH_TOKEN).",
+ endpoint: SANITY_CONTEXT_MCP_URL,
+ }),
+ },
+ ],
+ isError: true,
+ };
+}
+
async function handleSchemaExplorer(
_client: ReturnType,
args: Record,