feat: add mcp server, reorganize repo
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,9 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
# This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||
|
||||
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
@@ -0,0 +1 @@
|
||||
@AGENTS.md
|
||||
@@ -0,0 +1,45 @@
|
||||
# Rubber Duck MCP
|
||||
|
||||
MCP-сервер для техники «резиновая уточка»: когда ИИ застревает в логическом цикле или склонен к
|
||||
галлюцинациям, он вызывает инструмент `quack()`, чтобы сформулировать мысль и продолжить.
|
||||
|
||||
Сервер работает в режиме **Streamable HTTP** (stateless), развёртывается на Vercel, использует
|
||||
`mcp-handler` v2 + `@modelcontextprotocol/server` v2.
|
||||
|
||||
## Эндпоинт
|
||||
|
||||
```
|
||||
POST /api/mcp
|
||||
```
|
||||
|
||||
Подключите этот URL как MCP-сервер (тип Streamable HTTP) в Cursor, Claude Desktop и других клиентах.
|
||||
|
||||
## Инструменты
|
||||
|
||||
- `quack` — возвращает кряк. Опциональный параметр `mood`: `happy | confused | excited | sleepy`.
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev # http://localhost:3000
|
||||
pnpm build
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## Проверка
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json, text/event-stream" \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"quack","arguments":{"mood":"happy"}}}'
|
||||
```
|
||||
|
||||
## Деплой на Vercel
|
||||
|
||||
```bash
|
||||
vercel --prod
|
||||
```
|
||||
|
||||
(`rootDirectory` проекта — `apps/mcp`.)
|
||||
@@ -0,0 +1,21 @@
|
||||
import { createMcpHandler } from "mcp-handler";
|
||||
import { registerDuckTools } from "@/lib/mcp";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const post = createMcpHandler(
|
||||
async (server) => {
|
||||
registerDuckTools(server);
|
||||
},
|
||||
{
|
||||
serverInfo: { name: "rubber-duck-mcp", version: "1.0.0" },
|
||||
},
|
||||
);
|
||||
|
||||
export async function GET(req: Request) {
|
||||
return post(req);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
return post(req);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,49 @@
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Rubber Duck MCP",
|
||||
description: "MCP server for rubber duck debugging — quack() to articulate your thinking.",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<main style={{ fontFamily: "system-ui, sans-serif", maxWidth: 640, margin: "0 auto", padding: "3rem 1rem" }}>
|
||||
<h1>Rubber Duck MCP</h1>
|
||||
<p>
|
||||
MCP-сервер для техники «резиновая уточка»: когда ИИ застревает, он вызывает инструмент{" "}
|
||||
<code>quack()</code>, чтобы сформулировать мысль и продолжить. Сервер работает в режиме
|
||||
Streamable HTTP и предназначен для подключения в Cursor, Claude Desktop и других MCP-клиентах.
|
||||
</p>
|
||||
<h2>Эндпоинт</h2>
|
||||
<p>
|
||||
<code>/api/mcp</code> — подключите этот URL как MCP-сервер (тип Streamable HTTP).
|
||||
</p>
|
||||
<h2>Инструменты</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<code>quack</code> — возвращает кряк. Опциональный параметр <code>mood</code>:{" "}
|
||||
<code>happy | confused | excited | sleepy</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/server";
|
||||
import { z } from "zod";
|
||||
|
||||
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<T>(arr: readonly T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
export function registerDuckTools(server: McpServer): void {
|
||||
server.registerTool(
|
||||
"quack",
|
||||
{
|
||||
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 }] };
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "mcp",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/server": "^2.0.0",
|
||||
"mcp-handler": "^2.1.1",
|
||||
"next": "16.3.3",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8",
|
||||
"zod": "^4.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"packageManager": "pnpm@11.20.0"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user