@guuey/create-agentic-app 0.1.3 → 0.2.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 (31) hide show
  1. package/README.md +10 -0
  2. package/dist/templates/claude-agent-sdk/AGENTS.md +135 -0
  3. package/dist/templates/claude-agent-sdk/README.md +45 -15
  4. package/dist/templates/claude-agent-sdk/guuey.json +2 -1
  5. package/dist/templates/claude-agent-sdk/mcps/todo/package.json +1 -1
  6. package/dist/templates/claude-agent-sdk/mcps/todo/tsup.config.ts +14 -0
  7. package/dist/templates/claude-agent-sdk/package.json +3 -3
  8. package/dist/templates/claude-agent-sdk/scripts/dev.mjs +4 -3
  9. package/dist/templates/claude-agent-sdk/src/agent-config.ts +26 -19
  10. package/dist/templates/claude-agent-sdk/web/package.json +1 -1
  11. package/dist/templates/google-adk/AGENTS.md +135 -0
  12. package/dist/templates/google-adk/README.md +33 -17
  13. package/dist/templates/google-adk/guuey.json +3 -2
  14. package/dist/templates/google-adk/mcps/todo/package.json +1 -1
  15. package/dist/templates/google-adk/mcps/todo/tsup.config.ts +14 -0
  16. package/dist/templates/google-adk/package.json +2 -2
  17. package/dist/templates/google-adk/scripts/dev.mjs +4 -3
  18. package/dist/templates/google-adk/src/agent.ts +6 -2
  19. package/dist/templates/google-adk/web/package.json +1 -1
  20. package/dist/templates/mcp-base/src/server.ts +49 -0
  21. package/dist/templates/openai-agents-sdk/AGENTS.md +135 -0
  22. package/dist/templates/openai-agents-sdk/README.md +45 -15
  23. package/dist/templates/openai-agents-sdk/guuey.json +18 -4
  24. package/dist/templates/openai-agents-sdk/mcps/todo/package.json +1 -1
  25. package/dist/templates/openai-agents-sdk/mcps/todo/tsup.config.ts +14 -0
  26. package/dist/templates/openai-agents-sdk/package.json +3 -3
  27. package/dist/templates/openai-agents-sdk/scripts/dev.mjs +4 -3
  28. package/dist/templates/openai-agents-sdk/src/agent-config.ts +26 -19
  29. package/dist/templates/openai-agents-sdk/tsup.config.ts +9 -2
  30. package/dist/templates/openai-agents-sdk/web/package.json +1 -1
  31. package/package.json +3 -2
package/README.md CHANGED
@@ -29,3 +29,13 @@ guuey deploy # hosted: agent + MCP servers live on guuey
29
29
  ```
30
30
  npx @guuey/create-agentic-app <dir> [--framework claude-agent-sdk|openai-agents-sdk] [--skip-install]
31
31
  ```
32
+
33
+ ## Binding to an existing app
34
+
35
+ `guuey deploy` (above) creates a new app on first run. To bind this
36
+ scaffold to an app you already have — including a no-code agent built in
37
+ [Studio](https://studio.guuey.com) — run `guuey pull --app-id <id>` instead: it
38
+ refreshes `guuey.json`'s `appId`, and for a Studio no-code app pulls the
39
+ Studio-authored system prompt/model/MCP servers down into the scaffold too.
40
+ There is no `guuey link` command — `guuey pull --app-id` is the only way
41
+ to bind a project to an existing app.
@@ -0,0 +1,135 @@
1
+ # AGENTS.md — steering for the coding agent working in this repo
2
+
3
+ You (the coding agent) are extending a guuey agentic app. Before you reach
4
+ for a database or a cache, read this file. The single most common mistake
5
+ here is standing up local Redis/SQLite/file-based state "just to get
6
+ something working" — **don't.** Every pod this project deploys to
7
+ (`src/worker.ts`'s agent pod — including every `kind: colocated` `mcps/*`
8
+ server auto-spawned inside it, like this scaffold's `mcps/todo` — and every
9
+ `mcps/*` server deployed with `kind: hosted`) is **ephemeral and horizontally
10
+ replaced**: gVisor-isolated,
11
+ scale-to-zero, ordinary process death on every deploy or idle timeout. A
12
+ local file or an in-process `Map` used as a database is not a shortcut —
13
+ it is silent data loss the first time the pod recycles.
14
+
15
+ Use this table to route persistence decisions. Pick a row, don't invent one.
16
+
17
+ | Need | Use | Why |
18
+ | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
19
+ | Small per-user state on an MCP server (idempotency keys, rate-limit counters, OAuth nonces, prefs, a few KB) | [`@guuey/state`](https://www.npmjs.com/package/@guuey/state) — `withGuueyContext(scopeFromAuthorization(...), fn)` | Zero auth code — guuey owns the identity rails. Durable, per-`(user, mcp)`, survives pod restarts. |
20
+ | Large or relational data (rows, queries, joins, anything you'd reach for Postgres/Redis for) | An external managed serverless DB — Neon, Upstash, Supabase, or Turso — wired in via `guuey mcp secrets set DATABASE_URL=... --server <id>` (hosted MCP) or `guuey env set DATABASE_URL=...` (the agent worker and its colocated MCP children — the pod's declared env/secrets are handed to every colocated child), consumed through the provider's serverless driver | Guuey **integrates, never operates** databases — that's the platform's hosting policy, not a v1 gap. Pick the provider's **`us-east-1`** region: a guuey-hosted MCP → provider hop is then same-region AWS backbone, single-digit-ms — comparable to a cross-AZ hop, and agent turns are already dominated by seconds of LLM inference. |
21
+ | Local Redis, SQLite, or any file written as a database | **Never.** Not for v1, not "temporarily." | Pods are ephemeral and horizontally replaced — a local store vanishes on the next deploy, restart, or scale event, with no warning to you or the user. |
22
+ | Per-user files/blobs from **inside the agent pod** (`src/worker.ts`) | [`@guuey/fs`](https://www.npmjs.com/package/@guuey/fs)'s `homeDir()`/`appDir()`/`sessionDir()`, or the raw `$GUUEY_HOME_DIR`/`$GUUEY_APP_DIR` env vars + `node:fs` | Every invoke already gets these three bound directories — `$GUUEY_HOME_DIR` is durable per-user, `$GUUEY_APP_DIR` is read-only shared, cwd is session scratch. No wrapper API required, but the helpers save you the env-var lookups. |
23
+ | Per-user files/blobs from **inside an MCP server** (`mcps/*`) | Not yet available — `@guuey/files` is planned but not shipped. Fall back to the managed-DB row above (e.g. store blobs in the provider's object storage) until it lands. | Don't build against an API that doesn't exist yet. |
24
+ | Conversation / chat history | **Do not re-implement.** The platform already persists it. | Guuey writes thread history to DynamoDB and streams it back over AppSync; agents that keep their own transcript store are duplicating (and diverging from) data the platform already owns. |
25
+
26
+ ## `@guuey/state`: the one-line pattern
27
+
28
+ `@guuey/state` only works inside an MCP server whose `guuey.json` entry is
29
+ **federated** — that's the opt-in. A `kind: colocated` entry (like
30
+ `mcps/todo` in this scaffold) or a `kind: hosted` entry is federated
31
+ automatically. If you add a `kind: external` entry (a server you host
32
+ yourself, reached by URL), you must set `federate: true` on it:
33
+
34
+ ```json
35
+ {
36
+ "kind": "external",
37
+ "url": "https://your-server.example.com",
38
+ "federate": true
39
+ }
40
+ ```
41
+
42
+ Without `federate: true` (or `kind: colocated`/`kind: hosted`/a `ggui`
43
+ URL), guuey has no
44
+ identity to hand your server — a plain external entry with static
45
+ `headers` gets **no token and no state.** The token guuey sends on every
46
+ request to a federated server IS the credential; there is no separate
47
+ API key to provision.
48
+
49
+ Inside a federated MCP server, wrap each request in
50
+ `withGuueyContext(scopeFromAuthorization(header), fn)` and use the
51
+ barrel-exported `kv` inside `fn` — that's the whole integration. One
52
+ complete, runnable example — an idempotency-key check on a tool call,
53
+ using the raw Node `http` transport this scaffold's `mcps/todo/src/server.ts`
54
+ already uses:
55
+
56
+ ```ts
57
+ import { createHash } from "node:crypto";
58
+ import type { IncomingMessage, ServerResponse } from "node:http";
59
+ import { withGuueyContext, scopeFromAuthorization, kv, QuotaExceededError } from "@guuey/state";
60
+
61
+ // Inside the per-request handler (see mcps/todo/src/server.ts for the
62
+ // full StreamableHTTPServerTransport wiring this drops into):
63
+ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
64
+ const authHeader = req.headers.authorization;
65
+ if (typeof authHeader !== "string") {
66
+ res.writeHead(401).end();
67
+ return;
68
+ }
69
+
70
+ await withGuueyContext(scopeFromAuthorization(authHeader), async () => {
71
+ // ... hand off to your MCP transport/tool dispatch here; the
72
+ // AsyncLocalStorage context set above is visible to every `kv.*`
73
+ // call for the lifetime of this request, however deep the call
74
+ // stack goes (tool handlers, helper functions, etc.).
75
+ await handleCreateOrder({ orderPayload: "..." });
76
+ });
77
+ }
78
+
79
+ async function handleCreateOrder(input: { orderPayload: string }): Promise<{ duplicate: boolean }> {
80
+ const idempotencyKey = `order:${createHash("sha256").update(input.orderPayload).digest("hex")}`;
81
+
82
+ if (await kv.has(idempotencyKey)) {
83
+ return { duplicate: true };
84
+ }
85
+
86
+ try {
87
+ // TTL is required on every `set` — no permanent keys. Pick a window
88
+ // that covers realistic retry storms; 1 hour is a reasonable default
89
+ // for an idempotency key.
90
+ await kv.set(idempotencyKey, true, { ttl: 60 * 60 });
91
+ } catch (err) {
92
+ if (err instanceof QuotaExceededError) {
93
+ // The (user, mcp) scope hit its 1 MiB cap. Surface this to the
94
+ // caller instead of silently dropping the idempotency guard.
95
+ throw new Error("state quota exceeded — cannot verify idempotency");
96
+ }
97
+ throw err;
98
+ }
99
+
100
+ // ... actually create the order here.
101
+ return { duplicate: false };
102
+ }
103
+ ```
104
+
105
+ `scopeFromAuthorization` derives `{ userId, mcpId }` from the inbound
106
+ `Authorization: Bearer <jwt>` header guuey sends on every federated call —
107
+ `userId` from the JWT's `sub`, `mcpId` deterministically from its `aud`
108
+ (your server's federated resource URL). You never assert either id
109
+ yourself; a mismatched or missing token is rejected server-side.
110
+
111
+ ## The hard caps (design your data model around these, don't work around them)
112
+
113
+ `@guuey/state` is a KV, not a database — enforced, not a suggestion:
114
+
115
+ | Cap | Value |
116
+ | ------------------ | --------------------------------------------- |
117
+ | Scope size | 1 MiB (per `(user, mcp)`, key bytes included) |
118
+ | Single value | 64 KiB |
119
+ | TTL | required, ≤ 90 days — no permanent keys |
120
+ | `keys()` page size | ≤ 1000 |
121
+ | `mget()` batch | ≤ 100 keys |
122
+ | Counters | safe integers only (`increment`/`decrement`) |
123
+
124
+ If your data model needs more than this — queries, joins, cross-user
125
+ data, anything past a long tail of small per-user facts — that's the
126
+ signal to use the managed-DB row above, not to fight the cap.
127
+
128
+ ## Chat history: really, don't
129
+
130
+ If you find yourself writing message transcripts to `@guuey/state`,
131
+ `$GUUEY_HOME_DIR`, or an external DB keyed by conversation, stop — that's
132
+ the platform's job. Guuey persists every thread to DynamoDB and streams
133
+ history back over AppSync subscriptions; the client and Portal already
134
+ read it from there. A parallel, agent-owned history store will drift from
135
+ the platform's copy and confuse users about which one is authoritative.
@@ -9,7 +9,7 @@ locally with one command, and deployable to guuey with one more.
9
9
  ```
10
10
  .
11
11
  ├── guuey.json # the deploy contract: agent framework/model, system prompt,
12
- │ # mcpServers (name → local dev port / hosted source), ggui config
12
+ │ # mcpServers (name → colocated source + devPort), ggui config
13
13
  ├── package.json # agent deps + the pnpm workspace root (workspaces: mcps/*, web)
14
14
  ├── src/worker.ts # your agent code (code-mode worker); build emits ./guuey.worker.js
15
15
  ├── prompts/system.md # system prompt, referenced from guuey.json#agent.systemPrompt.file
@@ -38,16 +38,19 @@ cp .env.example .env.local # done automatically on scaffold if .env.local is a
38
38
  pnpm dev
39
39
  ```
40
40
 
41
- `pnpm dev` (`scripts/dev.mjs`) boots five processes with prefixed, interleaved
41
+ `pnpm dev` (`scripts/dev.mjs`) boots four processes with prefixed, interleaved
42
42
  logs. Ctrl-C tears all of them down together.
43
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 |
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; auto-spawns every `colocated` MCP entry — `mcps/todo` lands on its `devPort` :6782 |
48
+ | `ggui serve` | :6781 | local generative-UI server, over `ggui/` |
49
+ | `web` | :6890 | the Vite chat SPA |
50
+
51
+ The todo MCP still answers on :6782 — it's just a supervised child of
52
+ `guuey dev` now (copy `mcps/todo/` to add your own server; new `colocated`
53
+ entries in `guuey.json` are auto-spawned the same way).
51
54
 
52
55
  Open http://localhost:6890 to chat with your agent locally.
53
56
 
@@ -68,9 +71,13 @@ that only matter once real users and real money are involved:
68
71
  DynamoDB so users can resume a thread.
69
72
 
70
73
  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
+ worlds; only how it's resolved differs. Locally, `guuey dev` auto-spawns each
75
+ `colocated` entry on its `devPort` and points the agent at
76
+ `localhost:<devPort>`; deployed, the same server code runs as a supervised
77
+ child inside your agent's pod, reached over the pod's loopback with a
78
+ per-request federated identity. (`hosted` entries deploy to the platform's
79
+ federated URLs; locally they need a `devPort` pointing at a server you run
80
+ yourself, or they're skipped with a warning.)
74
81
 
75
82
  ## Deploying
76
83
 
@@ -83,17 +90,40 @@ guuey deploy # ships everything
83
90
  anything user-visible changes:
84
91
 
85
92
  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`.
93
+ as its own hosted MCP server (build → deploy → registry), then writes the
94
+ resulting server id back into `guuey.json`. This scaffold has none — the
95
+ todo MCP is `colocated`, so this leg is a no-op here.
88
96
  2. **ggui asset leg** — pushes `ggui/` (ggui.json, blueprints, themes) to your
89
97
  app's guuey-managed ggui instance.
90
98
  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.
99
+ project root (including `mcps/`), and deploys it as a gVisor-isolated,
100
+ scale-to-zero pod. Each `colocated` entry (e.g. `mcps/todo`) is built into
101
+ the worker image and auto-spawned as a supervised, sandboxed child at pod
102
+ boot — no separate server, registry row, or write-back.
92
103
  4. **Output** — prints your agent's endpoint URL and a Portal deep link.
93
104
 
94
105
  Re-running `guuey deploy` converges: unchanged pieces are skipped or reused,
95
106
  nothing is duplicated.
96
107
 
108
+ ## Filesystem + memory
109
+
110
+ Every deployed invoke gets three bound directories — `$GUUEY_HOME_DIR`
111
+ (durable, per-user), `$GUUEY_APP_DIR` (read-only, shared), and cwd (session
112
+ scratch) — plain `node:fs`, no wrapper API required. Signed-in users get
113
+ durable cross-session memory for free (a platform-owned prompt tells the
114
+ model to read/write `$GUUEY_HOME_DIR/memories/MEMORY.md`); guests never get
115
+ durable storage, by design. **This recall is `claude-agent-sdk`-only today**
116
+ — openai-agents-sdk and google-adk agents get the three bound directories
117
+ but not the automatic memory-file recall; all-framework support arrives
118
+ with guuey's own memory MCP (a platform tool every framework can call over
119
+ its existing MCP channel). Framework-native memory backends (e.g. ADK's
120
+ Vertex MemoryBank) are unsupported on Guuey — they'd store user data
121
+ outside guuey's deletion boundary. Full contract, code examples, and
122
+ rollout status: the "Your agent's filesystem" section of the guuey
123
+ monorepo's `docs/quickstart.md`, or
124
+ [`@guuey/fs`](https://www.npmjs.com/package/@guuey/fs)'s own README — an
125
+ optional, three-helper sugar layer over the same paths.
126
+
97
127
  ## How people talk to your agent
98
128
 
99
129
  - **guuey Portal** — a Telegram-like agent App Store and universal chat
@@ -6,7 +6,8 @@
6
6
  "model": "claude-sonnet-5",
7
7
  "systemPrompt": { "file": "prompts/system.md" },
8
8
  "mcpServers": {
9
- "todo": { "kind": "hosted", "source": "./mcps/todo", "devPort": 6782 }
9
+ "todo": { "kind": "colocated", "source": "./mcps/todo", "devPort": 6782 },
10
+ "ggui": { "kind": "external", "url": "https://mcp.ggui.ai", "transport": "http" }
10
11
  }
11
12
  },
12
13
  "ggui": { "configFile": "./ggui/ggui.json" }
@@ -5,7 +5,7 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "tsx src/server.ts",
8
- "build": "tsup src/server.ts --format esm -d dist",
8
+ "build": "tsup",
9
9
  "start": "node dist/server.js",
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ // Self-contained ON PURPOSE — do not delete this file. Without a local
4
+ // config, tsup's upward config discovery finds the scaffold ROOT
5
+ // tsup.config.ts; its bundled form imports 'tsup' from the scaffold root,
6
+ // which the platform image build deliberately leaves un-installed when the
7
+ // bundled guuey.worker.js skips the root install — every colocated build
8
+ // then dies with ERR_MODULE_NOT_FOUND (guuey#19). A config here stops the
9
+ // walk; `defineConfig` resolves from this package's own devDependencies.
10
+ export default defineConfig({
11
+ entry: ['src/server.ts'],
12
+ format: 'esm',
13
+ outDir: 'dist',
14
+ });
@@ -10,11 +10,11 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@anthropic-ai/claude-agent-sdk": "^0.3.199",
13
- "@guuey/config": "0.1.1",
14
- "@guuey/worker": "0.1.1"
13
+ "@guuey/config": "0.2.0",
14
+ "@guuey/worker": "0.2.0"
15
15
  },
16
16
  "devDependencies": {
17
- "@guuey/cli": "0.1.3",
17
+ "@guuey/cli": "0.2.0",
18
18
  "tsup": "^8.5.0",
19
19
  "tsx": "^4.19.0",
20
20
  "typescript": "^5.8.0"
@@ -29,10 +29,11 @@ if (!existsSync(".env.local"))
29
29
  console.warn("hint: cp .env.example .env.local and set your LLM key");
30
30
 
31
31
  boot("worker", "pnpm", ["exec", "tsup", "--watch"]); // rebuilds guuey.worker.js on change
32
+ // `guuey dev` auto-spawns every `kind: 'colocated'` mcpServers entry itself
33
+ // (name→localhost devPort resolution) — the todo MCP is colocated, so it no
34
+ // longer needs its own boot() here; a manual second spawn would double-bind
35
+ // :6782 and crash with EADDRINUSE.
32
36
  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
37
  boot("ggui", "pnpm", ["exec", "ggui", "serve", "--mcp-only", "--dev-allow-all", "--port", "6781"], {
37
38
  cwd: "ggui",
38
39
  });
@@ -7,7 +7,7 @@
7
7
  import { readFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { parseGuueyJson, type GuueyAgent } from "@guuey/config";
10
- import type { Invoke } from "@guuey/worker";
10
+ import { listCredentials, type Invoke } from "@guuey/worker";
11
11
 
12
12
  /** GUUEY_AGENT_SNAPSHOT (set by the platform pod and by `guuey dev`) wins; guuey.json is the fallback. */
13
13
  export function loadAgent(invoke: Invoke): GuueyAgent {
@@ -26,9 +26,9 @@ export function systemPrompt(invoke: Invoke, agent: GuueyAgent): string | undefi
26
26
 
27
27
  /**
28
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
29
+ * alongside `url`/`headers` (not hardcoded to `"http"`) because a credential
30
+ * file — `<session>/.guuey/credentials/<name>.json`, shape
31
+ * `{url, transport, headers}` per `@guuey/worker`'s `CredentialFile` — may
32
32
  * resolve a server onto the `sse` transport arm.
33
33
  */
34
34
  export interface McpEndpoint {
@@ -37,26 +37,33 @@ export interface McpEndpoint {
37
37
  headers: Record<string, string>;
38
38
  }
39
39
 
40
- /** Lowered entries are `external`; federation credentials (if any) arrive as per-session files. */
40
+ /**
41
+ * Endpoint set = broker-written credential DIRECTORY (source of truth for
42
+ * federated + platform-injected servers — incl. guuey-memory/guuey-profile,
43
+ * invisible to the declared map by design) ∪ declared external entries that
44
+ * have no credential file (plain endpoints, static headers). Credential wins
45
+ * per name. One honest log line names both sets — no silent fallback.
46
+ */
41
47
  export function mcpEndpoints(invoke: Invoke, agent: GuueyAgent): Record<string, McpEndpoint> {
42
48
  const out: Record<string, McpEndpoint> = {};
43
49
  for (const [name, entry] of Object.entries(agent.mcpServers ?? {})) {
50
+ if (entry === false) continue; // `ggui: false` is the generative-UI opt-out, not a server entry
44
51
  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 };
52
+ out[name] = {
53
+ url: entry.url,
54
+ transport: entry.transport ?? "http",
55
+ headers: { ...(entry.headers ?? {}) },
56
+ };
59
57
  }
58
+ const creds = listCredentials(invoke.fs)();
59
+ for (const { name, cred } of creds) {
60
+ out[name] = { url: cred.url, transport: cred.transport, headers: cred.headers };
61
+ }
62
+ const credentialed = creds.map((c) => c.name);
63
+ const declaredOnly = Object.keys(out).filter((n) => !credentialed.includes(n));
64
+ console.log(
65
+ `[guuey] mcp endpoints: credentialed=[${credentialed.join(",")}] declared-only=[${declaredOnly.join(",")}]`
66
+ );
60
67
  return out;
61
68
  }
62
69
 
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@mcp-ui/client": "^7.1.1",
13
- "@silverprotocol/core": "0.3.2",
13
+ "@silverprotocol/core": "0.4.1",
14
14
  "react": "^19.0.0",
15
15
  "react-dom": "^19.0.0"
16
16
  },
@@ -0,0 +1,135 @@
1
+ # AGENTS.md — steering for the coding agent working in this repo
2
+
3
+ You (the coding agent) are extending a guuey agentic app. Before you reach
4
+ for a database or a cache, read this file. The single most common mistake
5
+ here is standing up local Redis/SQLite/file-based state "just to get
6
+ something working" — **don't.** Every pod this project deploys to
7
+ (`src/worker.ts`'s agent pod — including every `kind: colocated` `mcps/*`
8
+ server auto-spawned inside it, like this scaffold's `mcps/todo` — and every
9
+ `mcps/*` server deployed with `kind: hosted`) is **ephemeral and horizontally
10
+ replaced**: gVisor-isolated,
11
+ scale-to-zero, ordinary process death on every deploy or idle timeout. A
12
+ local file or an in-process `Map` used as a database is not a shortcut —
13
+ it is silent data loss the first time the pod recycles.
14
+
15
+ Use this table to route persistence decisions. Pick a row, don't invent one.
16
+
17
+ | Need | Use | Why |
18
+ | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
19
+ | Small per-user state on an MCP server (idempotency keys, rate-limit counters, OAuth nonces, prefs, a few KB) | [`@guuey/state`](https://www.npmjs.com/package/@guuey/state) — `withGuueyContext(scopeFromAuthorization(...), fn)` | Zero auth code — guuey owns the identity rails. Durable, per-`(user, mcp)`, survives pod restarts. |
20
+ | Large or relational data (rows, queries, joins, anything you'd reach for Postgres/Redis for) | An external managed serverless DB — Neon, Upstash, Supabase, or Turso — wired in via `guuey mcp secrets set DATABASE_URL=... --server <id>` (hosted MCP) or `guuey env set DATABASE_URL=...` (the agent worker and its colocated MCP children — the pod's declared env/secrets are handed to every colocated child), consumed through the provider's serverless driver | Guuey **integrates, never operates** databases — that's the platform's hosting policy, not a v1 gap. Pick the provider's **`us-east-1`** region: a guuey-hosted MCP → provider hop is then same-region AWS backbone, single-digit-ms — comparable to a cross-AZ hop, and agent turns are already dominated by seconds of LLM inference. |
21
+ | Local Redis, SQLite, or any file written as a database | **Never.** Not for v1, not "temporarily." | Pods are ephemeral and horizontally replaced — a local store vanishes on the next deploy, restart, or scale event, with no warning to you or the user. |
22
+ | Per-user files/blobs from **inside the agent pod** (`src/worker.ts`) | [`@guuey/fs`](https://www.npmjs.com/package/@guuey/fs)'s `homeDir()`/`appDir()`/`sessionDir()`, or the raw `$GUUEY_HOME_DIR`/`$GUUEY_APP_DIR` env vars + `node:fs` | Every invoke already gets these three bound directories — `$GUUEY_HOME_DIR` is durable per-user, `$GUUEY_APP_DIR` is read-only shared, cwd is session scratch. No wrapper API required, but the helpers save you the env-var lookups. |
23
+ | Per-user files/blobs from **inside an MCP server** (`mcps/*`) | Not yet available — `@guuey/files` is planned but not shipped. Fall back to the managed-DB row above (e.g. store blobs in the provider's object storage) until it lands. | Don't build against an API that doesn't exist yet. |
24
+ | Conversation / chat history | **Do not re-implement.** The platform already persists it. | Guuey writes thread history to DynamoDB and streams it back over AppSync; agents that keep their own transcript store are duplicating (and diverging from) data the platform already owns. |
25
+
26
+ ## `@guuey/state`: the one-line pattern
27
+
28
+ `@guuey/state` only works inside an MCP server whose `guuey.json` entry is
29
+ **federated** — that's the opt-in. A `kind: colocated` entry (like
30
+ `mcps/todo` in this scaffold) or a `kind: hosted` entry is federated
31
+ automatically. If you add a `kind: external` entry (a server you host
32
+ yourself, reached by URL), you must set `federate: true` on it:
33
+
34
+ ```json
35
+ {
36
+ "kind": "external",
37
+ "url": "https://your-server.example.com",
38
+ "federate": true
39
+ }
40
+ ```
41
+
42
+ Without `federate: true` (or `kind: colocated`/`kind: hosted`/a `ggui`
43
+ URL), guuey has no
44
+ identity to hand your server — a plain external entry with static
45
+ `headers` gets **no token and no state.** The token guuey sends on every
46
+ request to a federated server IS the credential; there is no separate
47
+ API key to provision.
48
+
49
+ Inside a federated MCP server, wrap each request in
50
+ `withGuueyContext(scopeFromAuthorization(header), fn)` and use the
51
+ barrel-exported `kv` inside `fn` — that's the whole integration. One
52
+ complete, runnable example — an idempotency-key check on a tool call,
53
+ using the raw Node `http` transport this scaffold's `mcps/todo/src/server.ts`
54
+ already uses:
55
+
56
+ ```ts
57
+ import { createHash } from "node:crypto";
58
+ import type { IncomingMessage, ServerResponse } from "node:http";
59
+ import { withGuueyContext, scopeFromAuthorization, kv, QuotaExceededError } from "@guuey/state";
60
+
61
+ // Inside the per-request handler (see mcps/todo/src/server.ts for the
62
+ // full StreamableHTTPServerTransport wiring this drops into):
63
+ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
64
+ const authHeader = req.headers.authorization;
65
+ if (typeof authHeader !== "string") {
66
+ res.writeHead(401).end();
67
+ return;
68
+ }
69
+
70
+ await withGuueyContext(scopeFromAuthorization(authHeader), async () => {
71
+ // ... hand off to your MCP transport/tool dispatch here; the
72
+ // AsyncLocalStorage context set above is visible to every `kv.*`
73
+ // call for the lifetime of this request, however deep the call
74
+ // stack goes (tool handlers, helper functions, etc.).
75
+ await handleCreateOrder({ orderPayload: "..." });
76
+ });
77
+ }
78
+
79
+ async function handleCreateOrder(input: { orderPayload: string }): Promise<{ duplicate: boolean }> {
80
+ const idempotencyKey = `order:${createHash("sha256").update(input.orderPayload).digest("hex")}`;
81
+
82
+ if (await kv.has(idempotencyKey)) {
83
+ return { duplicate: true };
84
+ }
85
+
86
+ try {
87
+ // TTL is required on every `set` — no permanent keys. Pick a window
88
+ // that covers realistic retry storms; 1 hour is a reasonable default
89
+ // for an idempotency key.
90
+ await kv.set(idempotencyKey, true, { ttl: 60 * 60 });
91
+ } catch (err) {
92
+ if (err instanceof QuotaExceededError) {
93
+ // The (user, mcp) scope hit its 1 MiB cap. Surface this to the
94
+ // caller instead of silently dropping the idempotency guard.
95
+ throw new Error("state quota exceeded — cannot verify idempotency");
96
+ }
97
+ throw err;
98
+ }
99
+
100
+ // ... actually create the order here.
101
+ return { duplicate: false };
102
+ }
103
+ ```
104
+
105
+ `scopeFromAuthorization` derives `{ userId, mcpId }` from the inbound
106
+ `Authorization: Bearer <jwt>` header guuey sends on every federated call —
107
+ `userId` from the JWT's `sub`, `mcpId` deterministically from its `aud`
108
+ (your server's federated resource URL). You never assert either id
109
+ yourself; a mismatched or missing token is rejected server-side.
110
+
111
+ ## The hard caps (design your data model around these, don't work around them)
112
+
113
+ `@guuey/state` is a KV, not a database — enforced, not a suggestion:
114
+
115
+ | Cap | Value |
116
+ | ------------------ | --------------------------------------------- |
117
+ | Scope size | 1 MiB (per `(user, mcp)`, key bytes included) |
118
+ | Single value | 64 KiB |
119
+ | TTL | required, ≤ 90 days — no permanent keys |
120
+ | `keys()` page size | ≤ 1000 |
121
+ | `mget()` batch | ≤ 100 keys |
122
+ | Counters | safe integers only (`increment`/`decrement`) |
123
+
124
+ If your data model needs more than this — queries, joins, cross-user
125
+ data, anything past a long tail of small per-user facts — that's the
126
+ signal to use the managed-DB row above, not to fight the cap.
127
+
128
+ ## Chat history: really, don't
129
+
130
+ If you find yourself writing message transcripts to `@guuey/state`,
131
+ `$GUUEY_HOME_DIR`, or an external DB keyed by conversation, stop — that's
132
+ the platform's job. Guuey persists every thread to DynamoDB and streams
133
+ history back over AppSync subscriptions; the client and Portal already
134
+ read it from there. A parallel, agent-owned history store will drift from
135
+ the platform's copy and confuse users about which one is authoritative.