diff --git a/apps/web/package.json b/apps/web/package.json index 84d0973..01e0acb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -16,12 +16,16 @@ "@sveltejs/adapter-vercel": "^6.3.4", "@sveltejs/kit": "^2.63.0", "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@types/node": "^20.19.43", "svelte": "^5.56.1", "svelte-check": "^4.6.0", "typescript": "^6.0.3", "vite": "^8.0.16" }, "dependencies": { - "@duck/types": "workspace:*" + "@duck/types": "workspace:*", + "@modelcontextprotocol/server": "^2.0.0", + "mcp-handler": "^2.1.1", + "zod": "^4.5.4" } } diff --git a/apps/web/src/lib/mcp.ts b/apps/web/src/lib/mcp.ts new file mode 100644 index 0000000..02eff79 --- /dev/null +++ b/apps/web/src/lib/mcp.ts @@ -0,0 +1,40 @@ +import type { McpServer } from "@modelcontextprotocol/server"; +import { z } from "zod"; + +export const QUACK_TOOL_NAME = "quack"; + +const QUACKS = { + happy: ["QUACK! 🎉", "Quack quack! 😄", "QUAAACK! 🦆✨"], + confused: ["Qu...ack?", "Quack? 🤔", "Quaaack...?"], + excited: ["QUACK QUACK QUACK!", "QUAAACK!! 🔥", "Quack! Quack! Quack!"], + sleepy: ["quack... 😴", "quaack... 💤", "quack. *yawn*"], + default: ["Quack!", "Quack quack!", "QUACK!", "Quaaack!", "Quack. 🦆"], +} as const; + +const MOODS = ["happy", "confused", "excited", "sleepy"] as const; + +function randomFrom(arr: readonly T[]): T { + return arr[Math.floor(Math.random() * arr.length)]; +} + +export function registerDuckTools(server: McpServer): void { + server.registerTool( + QUACK_TOOL_NAME, + { + title: "Quack", + description: + "Rubber duck quacks. Use when you're stuck, looping, or hallucinating. " + + "The act of calling forces you to articulate your current thinking out loud.", + inputSchema: z.object({ + mood: z + .enum(MOODS) + .optional() + .describe("Mood of the duck. Omit for a random quack."), + }), + }, + async ({ mood }) => { + const text = mood ? randomFrom(QUACKS[mood]) : randomFrom(QUACKS.default); + return { content: [{ type: "text", text }] }; + }, + ); +} diff --git a/apps/web/src/routes/api/mcp/+server.ts b/apps/web/src/routes/api/mcp/+server.ts new file mode 100644 index 0000000..cb3014f --- /dev/null +++ b/apps/web/src/routes/api/mcp/+server.ts @@ -0,0 +1,20 @@ +import type { RequestEvent } from "@sveltejs/kit"; +import { createMcpHandler } from "mcp-handler"; +import { registerDuckTools } from "$lib/mcp"; + +const handler = createMcpHandler( + async (server) => { + registerDuckTools(server); + }, + { + serverInfo: { name: "rubber-duck-mcp", version: "1.0.0" }, + }, +); + +export function GET(event: RequestEvent): Promise { + return handler(event.request); +} + +export function POST(event: RequestEvent): Promise { + return handler(event.request); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ff8dea..996eeb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -268,6 +268,15 @@ importers: '@duck/types': specifier: workspace:* version: link:../../packages/types + '@modelcontextprotocol/server': + specifier: ^2.0.0 + version: 2.0.0 + mcp-handler: + specifier: ^2.1.1 + version: 2.1.1(@modelcontextprotocol/server@2.0.0)(next@16.3.3(@types/node@20.19.43)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)) + zod: + specifier: ^4.5.4 + version: 4.5.4 devDependencies: '@sveltejs/adapter-vercel': specifier: ^6.3.4 @@ -278,6 +287,9 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: ^7.1.2 version: 7.3.0(svelte@5.57.0)(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.2)(tsx@4.23.13)) + '@types/node': + specifier: ^20.19.43 + version: 20.19.43 svelte: specifier: ^5.56.1 version: 5.57.0