@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,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
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.199",
|
|
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
packages:
|
|
2
|
+
- "mcps/*"
|
|
3
|
+
- "web"
|
|
4
|
+
|
|
5
|
+
# pnpm 11 default `strictDepBuilds: true` blocks install scripts for any
|
|
6
|
+
# dependency with a build script (esbuild's native binary fetch, sharp,
|
|
7
|
+
# etc.) unless individually allowlisted, and fails the whole `install`
|
|
8
|
+
# with ERR_PNPM_IGNORED_BUILDS otherwise. Scaffolded apps pull in build-
|
|
9
|
+
# script deps transitively (tsup/vite -> esbuild, mcp/ui deps, etc.) that
|
|
10
|
+
# we can't enumerate up front, so match pnpm 9/10 behavior here.
|
|
11
|
+
strictDepBuilds: false
|
|
12
|
+
|
|
13
|
+
# pnpm 11 default `verifyDepsBeforeRun: 'install'` re-verifies the
|
|
14
|
+
# lockfile against supply-chain policies (including minimumReleaseAge)
|
|
15
|
+
# before every `pnpm run <script>`, hitting the registry even for
|
|
16
|
+
# already-installed deps. That check 404s for any dependency that only
|
|
17
|
+
# exists as a local tarball (not yet published), which is expected
|
|
18
|
+
# during local/dev/CI installs. Restore pnpm 9/10 behavior: only verify
|
|
19
|
+
# on an explicit `pnpm install`.
|
|
20
|
+
verifyDepsBeforeRun: false
|
|
21
|
+
|
|
22
|
+
# See above — same registry-lookback problem hits a plain `pnpm install`
|
|
23
|
+
# too when minimumReleaseAge is nonzero; disable it for scaffolded apps.
|
|
24
|
+
minimumReleaseAge: 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pnpm dev — boots the whole local stack. Ctrl-C tears everything down.
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
const procs = [];
|
|
7
|
+
function boot(name, command, args, opts = {}) {
|
|
8
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], ...opts });
|
|
9
|
+
const prefix = `[${name}]`.padEnd(9);
|
|
10
|
+
child.stdout.on("data", (d) => process.stdout.write(String(d).replace(/^/gm, prefix)));
|
|
11
|
+
child.stderr.on("data", (d) => process.stderr.write(String(d).replace(/^/gm, prefix)));
|
|
12
|
+
child.on("exit", (code) => {
|
|
13
|
+
if (code !== 0 && !shuttingDown) shutdown(`${name} exited (${code})`, 1);
|
|
14
|
+
});
|
|
15
|
+
procs.push(child);
|
|
16
|
+
}
|
|
17
|
+
let shuttingDown = false;
|
|
18
|
+
function shutdown(reason, code = 0) {
|
|
19
|
+
if (shuttingDown) return;
|
|
20
|
+
shuttingDown = true;
|
|
21
|
+
console.log(`\n${reason} — shutting down`);
|
|
22
|
+
for (const p of procs) p.kill("SIGTERM");
|
|
23
|
+
setTimeout(() => process.exit(code), 500);
|
|
24
|
+
}
|
|
25
|
+
process.on("SIGINT", () => shutdown("interrupted"));
|
|
26
|
+
process.on("SIGTERM", () => shutdown("terminated"));
|
|
27
|
+
|
|
28
|
+
if (!existsSync(".env.local"))
|
|
29
|
+
console.warn("hint: cp .env.example .env.local and set your LLM key");
|
|
30
|
+
|
|
31
|
+
boot("worker", "pnpm", ["exec", "tsup", "--watch"]); // rebuilds guuey.worker.js on change
|
|
32
|
+
boot("agent", "pnpm", ["exec", "guuey", "dev", "--serve", "--port", "6790"]);
|
|
33
|
+
boot("todo", "pnpm", ["--filter", "@agentic-app-template/todo-mcp", "dev"], {
|
|
34
|
+
env: { ...process.env, PORT: "6782" },
|
|
35
|
+
});
|
|
36
|
+
boot("ggui", "pnpm", ["exec", "ggui", "serve", "--mcp-only", "--dev-allow-all", "--port", "6781"], {
|
|
37
|
+
cwd: "ggui",
|
|
38
|
+
});
|
|
39
|
+
boot("web", "pnpm", ["--filter", "@agentic-app-template/web", "dev"], {
|
|
40
|
+
env: { ...process.env, PORT: "6890" },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log("\n agent http://localhost:6790 todo-mcp http://localhost:6782");
|
|
44
|
+
console.log(" ggui http://localhost:6781 web http://localhost:6890\n");
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot + system-prompt + MCP-endpoint resolution shared by both framework
|
|
3
|
+
* overlays. Duplicated verbatim in `templates-src/frameworks/openai-agents-sdk`
|
|
4
|
+
* by design — template code must be self-contained (no cross-overlay imports),
|
|
5
|
+
* so keep the two copies byte-identical when editing either one.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { parseGuueyJson, type GuueyAgent } from "@guuey/config";
|
|
10
|
+
import type { Invoke } from "@guuey/worker";
|
|
11
|
+
|
|
12
|
+
/** GUUEY_AGENT_SNAPSHOT (set by the platform pod and by `guuey dev`) wins; guuey.json is the fallback. */
|
|
13
|
+
export function loadAgent(invoke: Invoke): GuueyAgent {
|
|
14
|
+
const env = process.env.GUUEY_AGENT_SNAPSHOT;
|
|
15
|
+
if (env) return JSON.parse(env) as GuueyAgent;
|
|
16
|
+
const raw = JSON.parse(readFileSync(join(invoke.fs.app, "guuey.json"), "utf8"));
|
|
17
|
+
return parseGuueyJson(raw).agent;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function systemPrompt(invoke: Invoke, agent: GuueyAgent): string | undefined {
|
|
21
|
+
const sp = agent.systemPrompt;
|
|
22
|
+
if (typeof sp === "string") return sp;
|
|
23
|
+
if (sp?.file) return readFileSync(join(invoke.fs.app, sp.file), "utf8");
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* One resolved MCP endpoint this worker may connect to. `transport` rides
|
|
29
|
+
* alongside `url`/`headers` (not hardcoded to `"http"`) because a federation
|
|
30
|
+
* credential file — `<session>/.guuey/credentials/<name>.json`, shape
|
|
31
|
+
* `{url, transport, headers}` per `@guuey/host`'s `CredentialFile` — may
|
|
32
|
+
* resolve a server onto the `sse` transport arm.
|
|
33
|
+
*/
|
|
34
|
+
export interface McpEndpoint {
|
|
35
|
+
url: string;
|
|
36
|
+
transport: "http" | "sse";
|
|
37
|
+
headers: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Lowered entries are `external`; federation credentials (if any) arrive as per-session files. */
|
|
41
|
+
export function mcpEndpoints(invoke: Invoke, agent: GuueyAgent): Record<string, McpEndpoint> {
|
|
42
|
+
const out: Record<string, McpEndpoint> = {};
|
|
43
|
+
for (const [name, entry] of Object.entries(agent.mcpServers ?? {})) {
|
|
44
|
+
if (entry.kind !== "external") continue; // hosted/proxied are lowered to external before a worker ever runs
|
|
45
|
+
let url = entry.url;
|
|
46
|
+
let transport: "http" | "sse" = entry.transport ?? "http";
|
|
47
|
+
let headers: Record<string, string> = { ...(entry.headers ?? {}) };
|
|
48
|
+
try {
|
|
49
|
+
const cred = JSON.parse(
|
|
50
|
+
readFileSync(join(invoke.fs.session, ".guuey", "credentials", `${name}.json`), "utf8")
|
|
51
|
+
) as { url: string; transport: "http" | "sse"; headers: Record<string, string> };
|
|
52
|
+
url = cred.url;
|
|
53
|
+
transport = cred.transport;
|
|
54
|
+
headers = cred.headers;
|
|
55
|
+
} catch {
|
|
56
|
+
// no credential file — plain external endpoint; static headers apply
|
|
57
|
+
}
|
|
58
|
+
out[name] = { url, transport, headers };
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Fold prior turns into a prompt preamble (v1 keep-it-simple history handling). */
|
|
64
|
+
export function withHistory(invoke: Invoke): string {
|
|
65
|
+
if (invoke.history.length === 0) return invoke.input;
|
|
66
|
+
const lines = invoke.history.map(
|
|
67
|
+
(h) => `${h.role === "agent" ? "Assistant" : "User"}: ${h.text}`
|
|
68
|
+
);
|
|
69
|
+
return `<conversation_history>\n${lines.join("\n")}\n</conversation_history>\n\n${invoke.input}`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentic-app-template's code-mode worker entry (Claude Agent SDK). Reads the
|
|
3
|
+
* resolved agent snapshot + MCP endpoints via `./agent-config.js`, drives one
|
|
4
|
+
* `query()` per invoke, and streams every native SDK message to the Router
|
|
5
|
+
* via `emit.native` (the field spellings below — `mcpServers`/`systemPrompt`/
|
|
6
|
+
* `allowedTools`/`maxTurns`, and the `type === "result" && subtype ===
|
|
7
|
+
* "success"` result-detection — mirror `@guuey/host`'s `buildOptions` /
|
|
8
|
+
* `runInvoke`, the platform's own consumer of this exact snapshot shape).
|
|
9
|
+
*/
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
12
|
+
import { serveNative } from "@guuey/worker";
|
|
13
|
+
import { loadAgent, systemPrompt, mcpEndpoints, withHistory } from "./agent-config.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This worker is tsup-bundled (`noExternal`), which breaks the SDK's own
|
|
17
|
+
* native-CLI lookup: the SDK resolves `claude-agent-sdk-<platform>-<arch>`
|
|
18
|
+
* relative to its OWN module location, and once inlined into
|
|
19
|
+
* `guuey.worker.js` that location is the bundle's — where no node_modules
|
|
20
|
+
* has the binary. Resolution order here:
|
|
21
|
+
* 1. `GUUEY_CLAUDE_CODE_EXECUTABLE` — set by the guuey pod Router, pointing
|
|
22
|
+
* at the platform image's binary (a deployed `/worker` has no
|
|
23
|
+
* node_modules at all).
|
|
24
|
+
* 2. Anchored resolve — find the *installed* SDK from the bundle's location
|
|
25
|
+
* (it is this app's direct dependency), then resolve the platform binary
|
|
26
|
+
* package from the SDK's real location, exactly as the unbundled SDK
|
|
27
|
+
* would. Covers local `guuey dev --serve`, where the bundle runs next to
|
|
28
|
+
* the app's node_modules.
|
|
29
|
+
* 3. undefined — let the SDK try (and fail with its actionable error).
|
|
30
|
+
*/
|
|
31
|
+
function claudeExecutable(): string | undefined {
|
|
32
|
+
if (process.env.GUUEY_CLAUDE_CODE_EXECUTABLE) return process.env.GUUEY_CLAUDE_CODE_EXECUTABLE;
|
|
33
|
+
try {
|
|
34
|
+
const sdkEntry = createRequire(import.meta.url).resolve("@anthropic-ai/claude-agent-sdk");
|
|
35
|
+
return createRequire(sdkEntry).resolve(
|
|
36
|
+
`@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}/claude`
|
|
37
|
+
);
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const pathToClaudeCodeExecutable = claudeExecutable();
|
|
43
|
+
|
|
44
|
+
await serveNative(
|
|
45
|
+
async (invoke, emit) => {
|
|
46
|
+
const agent = loadAgent(invoke);
|
|
47
|
+
const mcpServers = Object.fromEntries(
|
|
48
|
+
Object.entries(mcpEndpoints(invoke, agent)).map(([name, ep]) => [
|
|
49
|
+
name,
|
|
50
|
+
// `alwaysLoad` — this agent's declared MCP servers ARE its tool
|
|
51
|
+
// surface; without it the CLI defers MCP tools behind its ToolSearch
|
|
52
|
+
// built-in (absent here — see `tools: []` below), leaving the model
|
|
53
|
+
// tool-less.
|
|
54
|
+
{ type: ep.transport, url: ep.url, headers: ep.headers, alwaysLoad: true },
|
|
55
|
+
])
|
|
56
|
+
);
|
|
57
|
+
let result = "";
|
|
58
|
+
for await (const message of query({
|
|
59
|
+
prompt: withHistory(invoke),
|
|
60
|
+
options: {
|
|
61
|
+
...(agent.model ? { model: agent.model } : {}),
|
|
62
|
+
...(systemPrompt(invoke, agent) ? { systemPrompt: systemPrompt(invoke, agent) } : {}),
|
|
63
|
+
mcpServers,
|
|
64
|
+
// Same posture as `@guuey/host`'s `buildOptions` (options.ts):
|
|
65
|
+
// - allowedTools: the snapshot allowlist verbatim, else every tool
|
|
66
|
+
// from every declared server (`mcp__<server>` prefix match) — MCP
|
|
67
|
+
// calls would otherwise hit the SDK's interactive ask stage, which
|
|
68
|
+
// nothing answers headless, so they'd be silently denied.
|
|
69
|
+
// - tools: [] — purely MCP-driven; no Bash/file/search built-ins.
|
|
70
|
+
// - settingSources: [] — never load the machine's ~/.claude or
|
|
71
|
+
// project settings into this worker (a dev box's logged-in Claude
|
|
72
|
+
// Code MCPs/plugins would leak into the tool catalog).
|
|
73
|
+
// - strictMcpConfig: true — the snapshot's servers only; ignore
|
|
74
|
+
// .mcp.json / user settings / plugins.
|
|
75
|
+
allowedTools: agent.tools?.allowlist ?? Object.keys(mcpServers).map((s) => `mcp__${s}`),
|
|
76
|
+
tools: [],
|
|
77
|
+
settingSources: [],
|
|
78
|
+
strictMcpConfig: true,
|
|
79
|
+
...(agent.runtime?.maxTurns ? { maxTurns: agent.runtime.maxTurns } : {}),
|
|
80
|
+
...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
|
|
81
|
+
},
|
|
82
|
+
})) {
|
|
83
|
+
emit.native(JSON.parse(JSON.stringify(message)));
|
|
84
|
+
if (message.type === "result" && message.subtype === "success") result = message.result;
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
},
|
|
88
|
+
{ framework: "claude-agent-sdk", sdkName: "@anthropic-ai/claude-agent-sdk" }
|
|
89
|
+
);
|
|
@@ -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
|
+
"noEmit": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: { "guuey.worker": "src/worker.ts" },
|
|
5
|
+
format: ["esm"],
|
|
6
|
+
outDir: ".",
|
|
7
|
+
clean: false,
|
|
8
|
+
noExternal: [/./], // bundle everything — the deploy tarball must be runnable via `node guuey.worker.js`
|
|
9
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Vite env for the web chat client. Copy to `web/.env.local` and UNCOMMENT
|
|
2
|
+
# the vars you want to override. (Vite reads env files from this directory —
|
|
3
|
+
# not the project root. Keep unused vars commented out: an empty
|
|
4
|
+
# `VITE_X=` declaration loads as "" — not undefined.)
|
|
5
|
+
|
|
6
|
+
# Agent base URL. Defaults to the local `guuey dev` router (:6790).
|
|
7
|
+
# VITE_AGENT_ENDPOINT_URL=http://localhost:6790
|
|
8
|
+
|
|
9
|
+
# Second-origin MCP-Apps sandbox page for @mcp-ui/client's <AppRenderer>.
|
|
10
|
+
# The MCP Apps spec requires UI resources to mount from a DIFFERENT origin
|
|
11
|
+
# than the app itself (double-iframe sandbox). In local dev this is served
|
|
12
|
+
# automatically on http://127.0.0.1:6891/sandbox.html by the
|
|
13
|
+
# sandboxProxyPlugin in vite.config.ts — leave unset. For a DEPLOYED build,
|
|
14
|
+
# host ../sandbox-proxy.ts's sandbox page on its own origin and point this
|
|
15
|
+
# at it; if unset in production, UI resources show a plain-text notice
|
|
16
|
+
# instead of rendering.
|
|
17
|
+
# VITE_SANDBOX_URL=https://sandbox.example.com/sandbox.html
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>agentic-app-template</title>
|
|
7
|
+
<meta name="description" content="Local dev chat client for your guuey agent." />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentic-app-template/web",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite --port 6890",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@mcp-ui/client": "^7.1.1",
|
|
13
|
+
"@silverprotocol/core": "0.1.0",
|
|
14
|
+
"react": "^19.0.0",
|
|
15
|
+
"react-dom": "^19.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.0.0",
|
|
19
|
+
"@types/react": "^19.0.0",
|
|
20
|
+
"@types/react-dom": "^19.0.0",
|
|
21
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
22
|
+
"typescript": "^5.8.0",
|
|
23
|
+
"vite": "^6.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|