feat: add mcp server, reorganize repo

This commit is contained in:
2026-09-03 17:20:08 +05:00
parent c1dda4d257
commit 1fd8ad6b50
48 changed files with 1691 additions and 12467 deletions
+41
View File
@@ -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
+9
View File
@@ -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 -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+45
View File
@@ -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`.)
+21
View File
@@ -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

+49
View File
@@ -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;
}
}
+15
View File
@@ -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>
);
}
+23
View File
@@ -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>
);
}
+38
View File
@@ -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 }] };
},
);
}
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+25
View File
@@ -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"
}
+34
View File
@@ -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"]
}