@guuey/create-agentic-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/dist/chunk-JD4RAKEG.js +203 -0
  4. package/dist/chunk-U7JDNY6B.js +149 -0
  5. package/dist/cli.cjs +293 -0
  6. package/dist/cli.d.cts +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +128 -0
  9. package/dist/index.cjs +233 -0
  10. package/dist/index.d.cts +76 -0
  11. package/dist/index.d.ts +76 -0
  12. package/dist/index.js +10 -0
  13. package/dist/templates/claude-agent-sdk/.env.example +4 -0
  14. package/dist/templates/claude-agent-sdk/.mcp.json +1 -0
  15. package/dist/templates/claude-agent-sdk/README.md +107 -0
  16. package/dist/templates/claude-agent-sdk/ggui/blueprints/.gitkeep +0 -0
  17. package/dist/templates/claude-agent-sdk/ggui/ggui.json +15 -0
  18. package/dist/templates/claude-agent-sdk/ggui/themes/.gitkeep +0 -0
  19. package/dist/templates/claude-agent-sdk/guuey.json +13 -0
  20. package/dist/templates/claude-agent-sdk/mcps/todo/Dockerfile +46 -0
  21. package/dist/templates/claude-agent-sdk/mcps/todo/package.json +22 -0
  22. package/dist/templates/claude-agent-sdk/mcps/todo/src/server.ts +173 -0
  23. package/dist/templates/claude-agent-sdk/mcps/todo/tsconfig.json +15 -0
  24. package/dist/templates/claude-agent-sdk/package.json +22 -0
  25. package/dist/templates/claude-agent-sdk/pnpm-workspace.yaml +24 -0
  26. package/dist/templates/claude-agent-sdk/prompts/system.md +4 -0
  27. package/dist/templates/claude-agent-sdk/scripts/dev.mjs +44 -0
  28. package/dist/templates/claude-agent-sdk/src/agent-config.ts +70 -0
  29. package/dist/templates/claude-agent-sdk/src/worker.ts +89 -0
  30. package/dist/templates/claude-agent-sdk/tsconfig.json +15 -0
  31. package/dist/templates/claude-agent-sdk/tsup.config.ts +9 -0
  32. package/dist/templates/claude-agent-sdk/web/.env.example +17 -0
  33. package/dist/templates/claude-agent-sdk/web/index.html +13 -0
  34. package/dist/templates/claude-agent-sdk/web/package.json +25 -0
  35. package/dist/templates/claude-agent-sdk/web/sandbox-proxy.ts +239 -0
  36. package/dist/templates/claude-agent-sdk/web/src/App.tsx +272 -0
  37. package/dist/templates/claude-agent-sdk/web/src/main.tsx +12 -0
  38. package/dist/templates/claude-agent-sdk/web/src/useAgentChat.ts +122 -0
  39. package/dist/templates/claude-agent-sdk/web/src/vite-env.d.ts +27 -0
  40. package/dist/templates/claude-agent-sdk/web/tsconfig.json +20 -0
  41. package/dist/templates/claude-agent-sdk/web/vite.config.ts +12 -0
  42. package/dist/templates/mcp-base/Dockerfile +46 -0
  43. package/dist/templates/mcp-base/package.json +22 -0
  44. package/dist/templates/mcp-base/src/server.ts +113 -0
  45. package/dist/templates/mcp-base/tsconfig.json +15 -0
  46. package/dist/templates/openai-agents-sdk/.env.example +4 -0
  47. package/dist/templates/openai-agents-sdk/.mcp.json +1 -0
  48. package/dist/templates/openai-agents-sdk/README.md +107 -0
  49. package/dist/templates/openai-agents-sdk/ggui/blueprints/.gitkeep +0 -0
  50. package/dist/templates/openai-agents-sdk/ggui/ggui.json +15 -0
  51. package/dist/templates/openai-agents-sdk/ggui/themes/.gitkeep +0 -0
  52. package/dist/templates/openai-agents-sdk/guuey.json +13 -0
  53. package/dist/templates/openai-agents-sdk/mcps/todo/Dockerfile +46 -0
  54. package/dist/templates/openai-agents-sdk/mcps/todo/package.json +22 -0
  55. package/dist/templates/openai-agents-sdk/mcps/todo/src/server.ts +173 -0
  56. package/dist/templates/openai-agents-sdk/mcps/todo/tsconfig.json +15 -0
  57. package/dist/templates/openai-agents-sdk/package.json +22 -0
  58. package/dist/templates/openai-agents-sdk/pnpm-workspace.yaml +24 -0
  59. package/dist/templates/openai-agents-sdk/prompts/system.md +4 -0
  60. package/dist/templates/openai-agents-sdk/scripts/dev.mjs +44 -0
  61. package/dist/templates/openai-agents-sdk/src/agent-config.ts +70 -0
  62. package/dist/templates/openai-agents-sdk/src/worker.ts +75 -0
  63. package/dist/templates/openai-agents-sdk/tsconfig.json +15 -0
  64. package/dist/templates/openai-agents-sdk/tsup.config.ts +9 -0
  65. package/dist/templates/openai-agents-sdk/web/.env.example +17 -0
  66. package/dist/templates/openai-agents-sdk/web/index.html +13 -0
  67. package/dist/templates/openai-agents-sdk/web/package.json +25 -0
  68. package/dist/templates/openai-agents-sdk/web/sandbox-proxy.ts +239 -0
  69. package/dist/templates/openai-agents-sdk/web/src/App.tsx +272 -0
  70. package/dist/templates/openai-agents-sdk/web/src/main.tsx +12 -0
  71. package/dist/templates/openai-agents-sdk/web/src/useAgentChat.ts +122 -0
  72. package/dist/templates/openai-agents-sdk/web/src/vite-env.d.ts +27 -0
  73. package/dist/templates/openai-agents-sdk/web/tsconfig.json +20 -0
  74. package/dist/templates/openai-agents-sdk/web/vite.config.ts +12 -0
  75. package/package.json +45 -0
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "useDefineForClassFields": true,
6
+ "allowJs": true,
7
+ "skipLibCheck": true,
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "module": "esnext",
12
+ "moduleResolution": "bundler",
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "jsx": "react-jsx",
16
+ "types": ["vite/client"]
17
+ },
18
+ "include": ["src", "vite.config.ts", "sandbox-proxy.ts"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import { sandboxProxyPlugin } from "./sandbox-proxy";
4
+
5
+ // Port is passed via the `dev` script's `--port 6890` flag (see package.json)
6
+ // so it stays visible in one place; `scripts/dev.mjs` boots this on the same
7
+ // port. The sandbox proxy plugin serves the MCP-Apps sandbox page on :6891 —
8
+ // a SECOND origin, as the spec's double-iframe architecture requires (see
9
+ // ./sandbox-proxy.ts).
10
+ export default defineConfig({
11
+ plugins: [react(), sandboxProxyPlugin()],
12
+ });
@@ -0,0 +1,46 @@
1
+ # NAME_PLACEHOLDER-mcp — guuey hosted MCP server image.
2
+ #
3
+ # Self-sufficient multi-stage build: `guuey mcp deploy` packs this directory
4
+ # into a tarball and hands it straight to the builder with no external
5
+ # pre-build step and no build args — this Dockerfile installs its own deps
6
+ # and builds itself. (Different from guuey's own first-party MCP servers,
7
+ # which use a "copy-prebuilt" pattern that expects `dist/` + `node_modules/`
8
+ # to already exist before `docker build` runs — that pattern does not apply
9
+ # to a standalone project like this one.)
10
+ #
11
+ # Build + run locally:
12
+ #
13
+ # docker build -t NAME_PLACEHOLDER-mcp .
14
+ # docker run -p 8080:8080 NAME_PLACEHOLDER-mcp
15
+ # curl http://localhost:8080/health
16
+ #
17
+ # Deploy to guuey:
18
+ #
19
+ # cd mcps/NAME_PLACEHOLDER && guuey mcp deploy
20
+
21
+ FROM node:22-slim AS build
22
+ WORKDIR /app
23
+ COPY package.json ./
24
+ RUN npm install
25
+ COPY tsconfig.json ./
26
+ COPY src ./src
27
+ RUN npm run build
28
+
29
+ FROM node:22-slim
30
+ WORKDIR /app
31
+ ENV NODE_ENV=production
32
+ # guuey's hosted-MCP runtime injects PORT=8080 and probes GET /health at that
33
+ # port — this default matches so `docker run` alone also works.
34
+ ENV PORT=8080
35
+
36
+ COPY package.json ./
37
+ RUN npm install --omit=dev
38
+ COPY --from=build /app/dist ./dist
39
+
40
+ USER node
41
+ EXPOSE 8080
42
+
43
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
44
+ CMD node -e "fetch('http://localhost:' + (process.env.PORT || 8080) + '/health').then(r => { if (!r.ok) throw 1 }).catch(() => process.exit(1))"
45
+
46
+ CMD ["node", "dist/server.js"]
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@agentic-app-template/NAME_PLACEHOLDER-mcp",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx src/server.ts",
8
+ "build": "tsup src/server.ts --format esm -d dist",
9
+ "start": "node dist/server.js",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@modelcontextprotocol/sdk": "^1.27.0",
14
+ "zod": "^4.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^22.0.0",
18
+ "tsx": "^4.19.0",
19
+ "tsup": "^8.5.0",
20
+ "typescript": "^5.8.0"
21
+ }
22
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * NAME_PLACEHOLDER-mcp — the copy-me starter MCP server for
3
+ * @agentic-app-template.
4
+ *
5
+ * A minimal, stateless Streamable HTTP MCP server with a single example
6
+ * tool (`echo`). Replace the example tool with your own real tools;
7
+ * everything else (transport wiring, `/health` + `PORT` contract) stays the
8
+ * same.
9
+ *
10
+ * Transport pattern mirrors guuey's canonical hosted-MCP scaffold
11
+ * (`mcp-servers/_template` in the guuey monorepo): a fresh `McpServer` +
12
+ * `StreamableHTTPServerTransport` are created **per request** and torn down
13
+ * when the response closes. Stateless mode — the guuey proxy (and, locally,
14
+ * whatever MCP client you point at this server) handles sessions, so this
15
+ * server never keeps request state around between calls.
16
+ *
17
+ * Runtime contract when deployed via `guuey mcp deploy` (see this
18
+ * directory's Dockerfile): the platform injects `PORT=8080` and probes
19
+ * `GET /health` for both liveness and readiness. Locally, `pnpm dev` runs
20
+ * this on `PORT=6782` when scaffolded as a workspace member of a guuey
21
+ * project (see ../../scripts/dev.mjs there).
22
+ */
23
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
24
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
25
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
26
+ import { z } from "zod";
27
+
28
+ const SERVER_NAME = "NAME_PLACEHOLDER-mcp";
29
+
30
+ /** Wrap a structured result as both `structuredContent` and a text block. */
31
+ function toolResult(data: Record<string, unknown>) {
32
+ return {
33
+ structuredContent: data,
34
+ content: [{ type: "text" as const, text: JSON.stringify(data) }],
35
+ };
36
+ }
37
+
38
+ function buildMcpServer(): McpServer {
39
+ const server = new McpServer({
40
+ name: SERVER_NAME,
41
+ version: "0.0.0",
42
+ description: "Example MCP server — replace the echo tool with your own.",
43
+ });
44
+
45
+ server.registerTool(
46
+ "echo",
47
+ {
48
+ title: "Echo",
49
+ description: "Echo a message back. Replace this with your own tool.",
50
+ inputSchema: { message: z.string().min(1).describe("Message to echo back.") },
51
+ outputSchema: { message: z.string() },
52
+ },
53
+ async ({ message }) => toolResult({ message }),
54
+ );
55
+
56
+ return server;
57
+ }
58
+
59
+ const PORT = Number(process.env.PORT ?? 6782);
60
+
61
+ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
62
+ if (req.method === "GET" && req.url === "/health") {
63
+ res.writeHead(200, { "content-type": "application/json" });
64
+ res.end(JSON.stringify({ status: "ok", server: SERVER_NAME, version: "0.0.0" }));
65
+ return;
66
+ }
67
+
68
+ if (req.url !== "/mcp") {
69
+ res.writeHead(404, { "content-type": "application/json" }).end(
70
+ JSON.stringify({ error: "not found" }),
71
+ );
72
+ return;
73
+ }
74
+
75
+ // Fresh server + transport per request (stateless mode) — see module doc.
76
+ const mcp = buildMcpServer();
77
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
78
+
79
+ res.on("close", () => {
80
+ transport.close().catch(() => undefined);
81
+ mcp.close().catch(() => undefined);
82
+ });
83
+
84
+ try {
85
+ await mcp.connect(transport);
86
+ await transport.handleRequest(req, res);
87
+ } catch (err) {
88
+ console.error("mcp_handle_failed", err);
89
+ if (!res.headersSent) {
90
+ res.writeHead(500, { "content-type": "application/json" });
91
+ res.end(
92
+ JSON.stringify({
93
+ jsonrpc: "2.0",
94
+ error: { code: -32603, message: "Internal server error" },
95
+ id: null,
96
+ }),
97
+ );
98
+ }
99
+ }
100
+ });
101
+
102
+ httpServer.listen(PORT, () => {
103
+ console.log(`${SERVER_NAME} listening on :${PORT}`);
104
+ });
105
+
106
+ function shutdown(signal: string): void {
107
+ console.log(`${signal} received — shutting down`);
108
+ httpServer.close(() => process.exit(0));
109
+ // Force-exit if connections don't drain in time.
110
+ setTimeout(() => process.exit(1), 10_000).unref();
111
+ }
112
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
113
+ process.on("SIGINT", () => shutdown("SIGINT"));
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }
@@ -0,0 +1,4 @@
1
+ # LLM key for local dev — the deployed agent uses guuey's managed broker instead.
2
+ ANTHROPIC_API_KEY=
3
+ # Only needed for the openai-agents-sdk template:
4
+ OPENAI_API_KEY=
@@ -0,0 +1 @@
1
+ { "mcpServers": { "ggui-dev": { "type": "http", "url": "https://mcp.ggui.ai/dev" } } }
@@ -0,0 +1,107 @@
1
+ # agentic-app-template
2
+
3
+ A guuey agentic app: a code-mode agent, a custom MCP server you can copy to
4
+ add your own tools, and a `ggui` config for generative UI — all runnable
5
+ locally with one command, and deployable to guuey with one more.
6
+
7
+ ## What got scaffolded
8
+
9
+ ```
10
+ .
11
+ ├── guuey.json # the deploy contract: agent framework/model, system prompt,
12
+ │ # mcpServers (name → local dev port / hosted source), ggui config
13
+ ├── package.json # agent deps + the pnpm workspace root (workspaces: mcps/*, web)
14
+ ├── src/worker.ts # your agent code (code-mode worker); build emits ./guuey.worker.js
15
+ ├── prompts/system.md # system prompt, referenced from guuey.json#agent.systemPrompt.file
16
+ ├── mcps/todo/ # the "copy-me" custom MCP server — @modelcontextprotocol/sdk,
17
+ │ # Streamable HTTP, in-memory todo list, port :6782
18
+ ├── ggui/ # ggui.json + blueprints/ + themes/ — generative-UI config.
19
+ │ # `ggui serve` runs against this locally; it's pushed to your
20
+ │ # deployed app's guuey-managed ggui instance on `guuey deploy`.
21
+ ├── web/ # a small Vite SPA chat client — the local dev surface, and a
22
+ │ # worked example of the bring-your-own-frontend path
23
+ ├── scripts/dev.mjs # `pnpm dev` orchestrator — boots the whole local stack
24
+ ├── .env.example # ANTHROPIC_API_KEY / OPENAI_API_KEY for local dev
25
+ └── .mcp.json # Claude Code convenience wiring (mcp.ggui.ai/dev)
26
+ ```
27
+
28
+ The project root **is** the agent package — `guuey deploy` packs this directory
29
+ directly as the deploy tarball, so `guuey.json` and `src/worker.ts` live at the top
30
+ level rather than nested under a `servers/` or `apps/` folder.
31
+
32
+ ## Quick start
33
+
34
+ ```bash
35
+ pnpm install
36
+ cp .env.example .env.local # done automatically on scaffold if .env.local is absent
37
+ # set ANTHROPIC_API_KEY (or OPENAI_API_KEY, for the openai-agents-sdk template) in .env.local
38
+ pnpm dev
39
+ ```
40
+
41
+ `pnpm dev` (`scripts/dev.mjs`) boots five processes with prefixed, interleaved
42
+ logs. Ctrl-C tears all of them down together.
43
+
44
+ | process | port | what |
45
+ | ------------ | ----- | ------------------------------------------------------------ |
46
+ | `worker` | — | `tsup --watch` — rebuilds `guuey.worker.js` on every save |
47
+ | `guuey dev` | :6790 | local router: spawns your worker per turn, streams SSE |
48
+ | `mcps/todo` | :6782 | the example MCP server (copy this directory to add your own) |
49
+ | `ggui serve` | :6781 | local generative-UI server, over `ggui/` |
50
+ | `web` | :6890 | the Vite chat SPA |
51
+
52
+ Open http://localhost:6890 to chat with your agent locally.
53
+
54
+ ## Local dev vs. deployed — what's different
55
+
56
+ `guuey dev` is the open, local equivalent of the pod router that runs your
57
+ worker in production. It mirrors the real spawn/stream contract closely — the
58
+ same normalizer, the same SSE shape — but it deliberately cuts three corners
59
+ that only matter once real users and real money are involved:
60
+
61
+ - **No sandboxing.** Locally your worker runs as a plain child process — no
62
+ `bwrap`/gVisor isolation. In production every invocation runs inside a
63
+ gVisor-isolated pod.
64
+ - **Permissive auth.** `ggui serve --dev-allow-all` accepts any bearer
65
+ locally. The deployed ggui instance enforces real auth.
66
+ - **No metering or history.** Local runs aren't billed and aren't persisted.
67
+ Deployed conversations are metered and their history is written to
68
+ DynamoDB so users can resume a thread.
69
+
70
+ None of this changes your code — `guuey.json` is the same file in both
71
+ worlds; only how it's resolved differs (`guuey dev` points MCP server names
72
+ at `localhost:<devPort>`, `guuey deploy` points them at the platform's
73
+ federated URLs).
74
+
75
+ ## Deploying
76
+
77
+ ```bash
78
+ guuey login # device-flow auth; stores a token in ~/.guuey/auth.json
79
+ guuey deploy # ships everything
80
+ ```
81
+
82
+ `guuey deploy` runs four legs, in order, so a hard failure aborts before
83
+ anything user-visible changes:
84
+
85
+ 1. **MCP leg** — deploys each `hosted` entry under `guuey.json#agent.mcpServers`
86
+ (e.g. `mcps/todo`) as its own hosted MCP server (build → deploy → registry),
87
+ then writes the resulting server id back into `guuey.json`.
88
+ 2. **ggui asset leg** — pushes `ggui/` (ggui.json, blueprints, themes) to your
89
+ app's guuey-managed ggui instance.
90
+ 3. **Agent leg** — builds `src/worker.ts` into `guuey.worker.js`, packs the
91
+ project root, and deploys it as a gVisor-isolated, scale-to-zero pod.
92
+ 4. **Output** — prints your agent's endpoint URL and a Portal deep link.
93
+
94
+ Re-running `guuey deploy` converges: unchanged pieces are skipped or reused,
95
+ nothing is duplicated.
96
+
97
+ ## How people talk to your agent
98
+
99
+ - **guuey Portal** — a Telegram-like agent App Store and universal chat
100
+ client. Once deployed, your agent is reachable from Portal with **zero
101
+ frontend code** — most builders never need to touch `web/` at all.
102
+ - **`web/`** — ships anyway, because the local dev loop needs a chat surface,
103
+ and it's a worked example of the bring-your-own-frontend path if you want
104
+ to embed your agent somewhere Portal doesn't reach.
105
+ - **Future: guuey widgets** — an embeddable web-chat widget for dropping
106
+ your agent into an existing site is planned as a separate feature; `web/`
107
+ is designed to make that graduation straightforward when it lands.
@@ -0,0 +1,15 @@
1
+ {
2
+ "schema": "1",
3
+ "protocol": "draft-2026-06-12",
4
+ "app": {
5
+ "slug": "agentic-app-template",
6
+ "name": "agentic-app-template"
7
+ },
8
+ "generation": {
9
+ "model": "anthropic:claude-haiku-4-5-20251001"
10
+ },
11
+ "theme": {
12
+ "preset": "indigo",
13
+ "mode": "dark"
14
+ }
15
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "schema": "1",
3
+ "agent": {
4
+ "mode": "code",
5
+ "framework": "openai-agents-sdk",
6
+ "model": "gpt-5.5",
7
+ "systemPrompt": { "file": "prompts/system.md" },
8
+ "mcpServers": {
9
+ "todo": { "kind": "hosted", "source": "./mcps/todo", "devPort": 6782 }
10
+ }
11
+ },
12
+ "ggui": { "configFile": "./ggui/ggui.json" }
13
+ }
@@ -0,0 +1,46 @@
1
+ # todo-mcp — guuey hosted MCP server image.
2
+ #
3
+ # Self-sufficient multi-stage build: `guuey mcp deploy` packs this directory
4
+ # into a tarball and hands it straight to the builder with no external
5
+ # pre-build step and no build args — this Dockerfile installs its own deps
6
+ # and builds itself. (Different from guuey's own first-party MCP servers,
7
+ # which use a "copy-prebuilt" pattern that expects `dist/` + `node_modules/`
8
+ # to already exist before `docker build` runs — that pattern does not apply
9
+ # to a standalone project like this one.)
10
+ #
11
+ # Build + run locally:
12
+ #
13
+ # docker build -t todo-mcp .
14
+ # docker run -p 8080:8080 todo-mcp
15
+ # curl http://localhost:8080/health
16
+ #
17
+ # Deploy to guuey:
18
+ #
19
+ # cd mcps/todo && guuey mcp deploy
20
+
21
+ FROM node:22-slim AS build
22
+ WORKDIR /app
23
+ COPY package.json ./
24
+ RUN npm install
25
+ COPY tsconfig.json ./
26
+ COPY src ./src
27
+ RUN npm run build
28
+
29
+ FROM node:22-slim
30
+ WORKDIR /app
31
+ ENV NODE_ENV=production
32
+ # guuey's hosted-MCP runtime injects PORT=8080 and probes GET /health at that
33
+ # port — this default matches so `docker run` alone also works.
34
+ ENV PORT=8080
35
+
36
+ COPY package.json ./
37
+ RUN npm install --omit=dev
38
+ COPY --from=build /app/dist ./dist
39
+
40
+ USER node
41
+ EXPOSE 8080
42
+
43
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
44
+ CMD node -e "fetch('http://localhost:' + (process.env.PORT || 8080) + '/health').then(r => { if (!r.ok) throw 1 }).catch(() => process.exit(1))"
45
+
46
+ CMD ["node", "dist/server.js"]
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@agentic-app-template/todo-mcp",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx src/server.ts",
8
+ "build": "tsup src/server.ts --format esm -d dist",
9
+ "start": "node dist/server.js",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@modelcontextprotocol/sdk": "^1.27.0",
14
+ "zod": "^4.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^22.0.0",
18
+ "tsx": "^4.19.0",
19
+ "tsup": "^8.5.0",
20
+ "typescript": "^5.8.0"
21
+ }
22
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * todo-mcp — the copy-me example MCP server for @agentic-app-template.
3
+ *
4
+ * A minimal, stateless Streamable HTTP MCP server: four tools over an
5
+ * in-memory todo list. Swap the `Map` for a real database when you outgrow
6
+ * a single replica; everything else (transport wiring, tool shape) stays
7
+ * the same.
8
+ *
9
+ * Transport pattern mirrors guuey's canonical hosted-MCP scaffold
10
+ * (`mcp-servers/_template` in the guuey monorepo): a fresh `McpServer` +
11
+ * `StreamableHTTPServerTransport` are created **per request** and torn down
12
+ * when the response closes. Stateless mode — the guuey proxy (and, locally,
13
+ * whatever MCP client you point at this server) handles sessions, so this
14
+ * server never keeps request state around between calls.
15
+ *
16
+ * Runtime contract when deployed via `guuey mcp deploy` (see mcps/todo's
17
+ * Dockerfile): the platform injects `PORT=8080` and probes `GET /health`
18
+ * for both liveness and readiness. Locally, `pnpm dev` runs this on
19
+ * `PORT=6782` (see ../../scripts/dev.mjs).
20
+ */
21
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
22
+ import { randomUUID } from "node:crypto";
23
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
25
+ import { z } from "zod";
26
+
27
+ interface Todo {
28
+ id: string;
29
+ title: string;
30
+ done: boolean;
31
+ }
32
+
33
+ const todos = new Map<string, Todo>();
34
+
35
+ const todoShape = {
36
+ id: z.string(),
37
+ title: z.string(),
38
+ done: z.boolean(),
39
+ };
40
+
41
+ /** Wrap a structured result as both `structuredContent` and a text block. */
42
+ function toolResult(data: Record<string, unknown>) {
43
+ return {
44
+ structuredContent: data,
45
+ content: [{ type: "text" as const, text: JSON.stringify(data) }],
46
+ };
47
+ }
48
+
49
+ function buildMcpServer(): McpServer {
50
+ const server = new McpServer({
51
+ name: "todo-mcp",
52
+ version: "0.0.0",
53
+ description: "Create, list, toggle, and delete todos.",
54
+ });
55
+
56
+ server.registerTool(
57
+ "todo_list",
58
+ {
59
+ title: "List todos",
60
+ description: "List every todo, in creation order.",
61
+ inputSchema: {},
62
+ outputSchema: { todos: z.array(z.object(todoShape)) },
63
+ },
64
+ async () => toolResult({ todos: [...todos.values()] }),
65
+ );
66
+
67
+ server.registerTool(
68
+ "todo_create",
69
+ {
70
+ title: "Create todo",
71
+ description: "Create a new todo item.",
72
+ inputSchema: { title: z.string().min(1).describe("Todo title.") },
73
+ outputSchema: todoShape,
74
+ },
75
+ async ({ title }) => {
76
+ const todo: Todo = { id: randomUUID(), title, done: false };
77
+ todos.set(todo.id, todo);
78
+ // Spread into a fresh object literal — `toolResult` takes
79
+ // `Record<string, unknown>`, and a named interface like `Todo` isn't
80
+ // structurally assignable to that without an (unwanted) index
81
+ // signature of its own; a fresh literal is.
82
+ return toolResult({ ...todo });
83
+ },
84
+ );
85
+
86
+ server.registerTool(
87
+ "todo_toggle",
88
+ {
89
+ title: "Toggle todo",
90
+ description: "Flip a todo's done state.",
91
+ inputSchema: { id: z.string().describe("Todo id.") },
92
+ outputSchema: todoShape,
93
+ },
94
+ async ({ id }) => {
95
+ const todo = todos.get(id);
96
+ if (!todo) throw new Error(`todo not found: ${id}`);
97
+ todo.done = !todo.done;
98
+ return toolResult({ ...todo }); // fresh literal — see the create() comment above
99
+ },
100
+ );
101
+
102
+ server.registerTool(
103
+ "todo_delete",
104
+ {
105
+ title: "Delete todo",
106
+ description: "Delete a todo by id.",
107
+ inputSchema: { id: z.string().describe("Todo id.") },
108
+ outputSchema: { id: z.string(), deleted: z.boolean() },
109
+ },
110
+ async ({ id }) => {
111
+ const deleted = todos.delete(id);
112
+ return toolResult({ id, deleted });
113
+ },
114
+ );
115
+
116
+ return server;
117
+ }
118
+
119
+ const PORT = Number(process.env.PORT ?? 6782);
120
+
121
+ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
122
+ if (req.method === "GET" && req.url === "/health") {
123
+ res.writeHead(200, { "content-type": "application/json" });
124
+ res.end(JSON.stringify({ status: "ok", server: "todo-mcp", version: "0.0.0" }));
125
+ return;
126
+ }
127
+
128
+ if (req.url !== "/mcp") {
129
+ res.writeHead(404, { "content-type": "application/json" }).end(
130
+ JSON.stringify({ error: "not found" }),
131
+ );
132
+ return;
133
+ }
134
+
135
+ // Fresh server + transport per request (stateless mode) — see module doc.
136
+ const mcp = buildMcpServer();
137
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
138
+
139
+ res.on("close", () => {
140
+ transport.close().catch(() => undefined);
141
+ mcp.close().catch(() => undefined);
142
+ });
143
+
144
+ try {
145
+ await mcp.connect(transport);
146
+ await transport.handleRequest(req, res);
147
+ } catch (err) {
148
+ console.error("mcp_handle_failed", err);
149
+ if (!res.headersSent) {
150
+ res.writeHead(500, { "content-type": "application/json" });
151
+ res.end(
152
+ JSON.stringify({
153
+ jsonrpc: "2.0",
154
+ error: { code: -32603, message: "Internal server error" },
155
+ id: null,
156
+ }),
157
+ );
158
+ }
159
+ }
160
+ });
161
+
162
+ httpServer.listen(PORT, () => {
163
+ console.log(`todo-mcp listening on :${PORT}`);
164
+ });
165
+
166
+ function shutdown(signal: string): void {
167
+ console.log(`${signal} received — shutting down`);
168
+ httpServer.close(() => process.exit(0));
169
+ // Force-exit if connections don't drain in time.
170
+ setTimeout(() => process.exit(1), 10_000).unref();
171
+ }
172
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
173
+ process.on("SIGINT", () => shutdown("SIGINT"));
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "agentic-app-template",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "node scripts/dev.mjs",
8
+ "build": "tsup",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@openai/agents": "^0.12.0",
13
+ "@guuey/config": "0.0.1",
14
+ "@guuey/worker": "0.0.1"
15
+ },
16
+ "devDependencies": {
17
+ "@guuey/cli": "0.0.1",
18
+ "tsup": "^8.5.0",
19
+ "tsx": "^4.19.0",
20
+ "typescript": "^5.8.0"
21
+ }
22
+ }