@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.
- package/LICENSE +21 -0
- package/README.md +31 -0
- package/dist/chunk-JD4RAKEG.js +203 -0
- package/dist/chunk-U7JDNY6B.js +149 -0
- package/dist/cli.cjs +293 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/index.cjs +233 -0
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +10 -0
- package/dist/templates/claude-agent-sdk/.env.example +4 -0
- package/dist/templates/claude-agent-sdk/.mcp.json +1 -0
- package/dist/templates/claude-agent-sdk/README.md +107 -0
- package/dist/templates/claude-agent-sdk/ggui/blueprints/.gitkeep +0 -0
- package/dist/templates/claude-agent-sdk/ggui/ggui.json +15 -0
- package/dist/templates/claude-agent-sdk/ggui/themes/.gitkeep +0 -0
- package/dist/templates/claude-agent-sdk/guuey.json +13 -0
- package/dist/templates/claude-agent-sdk/mcps/todo/Dockerfile +46 -0
- package/dist/templates/claude-agent-sdk/mcps/todo/package.json +22 -0
- package/dist/templates/claude-agent-sdk/mcps/todo/src/server.ts +173 -0
- package/dist/templates/claude-agent-sdk/mcps/todo/tsconfig.json +15 -0
- package/dist/templates/claude-agent-sdk/package.json +22 -0
- package/dist/templates/claude-agent-sdk/pnpm-workspace.yaml +24 -0
- package/dist/templates/claude-agent-sdk/prompts/system.md +4 -0
- package/dist/templates/claude-agent-sdk/scripts/dev.mjs +44 -0
- package/dist/templates/claude-agent-sdk/src/agent-config.ts +70 -0
- package/dist/templates/claude-agent-sdk/src/worker.ts +89 -0
- package/dist/templates/claude-agent-sdk/tsconfig.json +15 -0
- package/dist/templates/claude-agent-sdk/tsup.config.ts +9 -0
- package/dist/templates/claude-agent-sdk/web/.env.example +17 -0
- package/dist/templates/claude-agent-sdk/web/index.html +13 -0
- package/dist/templates/claude-agent-sdk/web/package.json +25 -0
- package/dist/templates/claude-agent-sdk/web/sandbox-proxy.ts +239 -0
- package/dist/templates/claude-agent-sdk/web/src/App.tsx +272 -0
- package/dist/templates/claude-agent-sdk/web/src/main.tsx +12 -0
- package/dist/templates/claude-agent-sdk/web/src/useAgentChat.ts +122 -0
- package/dist/templates/claude-agent-sdk/web/src/vite-env.d.ts +27 -0
- package/dist/templates/claude-agent-sdk/web/tsconfig.json +20 -0
- package/dist/templates/claude-agent-sdk/web/vite.config.ts +12 -0
- package/dist/templates/mcp-base/Dockerfile +46 -0
- package/dist/templates/mcp-base/package.json +22 -0
- package/dist/templates/mcp-base/src/server.ts +113 -0
- package/dist/templates/mcp-base/tsconfig.json +15 -0
- package/dist/templates/openai-agents-sdk/.env.example +4 -0
- package/dist/templates/openai-agents-sdk/.mcp.json +1 -0
- package/dist/templates/openai-agents-sdk/README.md +107 -0
- package/dist/templates/openai-agents-sdk/ggui/blueprints/.gitkeep +0 -0
- package/dist/templates/openai-agents-sdk/ggui/ggui.json +15 -0
- package/dist/templates/openai-agents-sdk/ggui/themes/.gitkeep +0 -0
- package/dist/templates/openai-agents-sdk/guuey.json +13 -0
- package/dist/templates/openai-agents-sdk/mcps/todo/Dockerfile +46 -0
- package/dist/templates/openai-agents-sdk/mcps/todo/package.json +22 -0
- package/dist/templates/openai-agents-sdk/mcps/todo/src/server.ts +173 -0
- package/dist/templates/openai-agents-sdk/mcps/todo/tsconfig.json +15 -0
- package/dist/templates/openai-agents-sdk/package.json +22 -0
- package/dist/templates/openai-agents-sdk/pnpm-workspace.yaml +24 -0
- package/dist/templates/openai-agents-sdk/prompts/system.md +4 -0
- package/dist/templates/openai-agents-sdk/scripts/dev.mjs +44 -0
- package/dist/templates/openai-agents-sdk/src/agent-config.ts +70 -0
- package/dist/templates/openai-agents-sdk/src/worker.ts +75 -0
- package/dist/templates/openai-agents-sdk/tsconfig.json +15 -0
- package/dist/templates/openai-agents-sdk/tsup.config.ts +9 -0
- package/dist/templates/openai-agents-sdk/web/.env.example +17 -0
- package/dist/templates/openai-agents-sdk/web/index.html +13 -0
- package/dist/templates/openai-agents-sdk/web/package.json +25 -0
- package/dist/templates/openai-agents-sdk/web/sandbox-proxy.ts +239 -0
- package/dist/templates/openai-agents-sdk/web/src/App.tsx +272 -0
- package/dist/templates/openai-agents-sdk/web/src/main.tsx +12 -0
- package/dist/templates/openai-agents-sdk/web/src/useAgentChat.ts +122 -0
- package/dist/templates/openai-agents-sdk/web/src/vite-env.d.ts +27 -0
- package/dist/templates/openai-agents-sdk/web/tsconfig.json +20 -0
- package/dist/templates/openai-agents-sdk/web/vite.config.ts +12 -0
- 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 @@
|
|
|
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.
|
|
File without changes
|
|
@@ -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
|
+
}
|
|
File without changes
|
|
@@ -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
|
+
}
|