@terreno/mcp 57.19.2 → 57.20.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/CHANGELOG.md +393 -0
- package/README.md +32 -269
- package/dist/bootstrap.d.ts +12 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +77 -122
- package/dist/bootstrap.js.map +1 -1
- package/dist/docs/guidelines/ui/guidelines/core.md +1 -1
- package/dist/docs/resources/overview.md +4 -2
- package/dist/docs/resources/patterns.md +237 -176
- package/dist/docs/resources/ui.md +48 -17
- package/dist/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
- package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
- package/dist/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
- package/dist/docs/upgrades/57.20.0.md +238 -0
- package/dist/docs/versioned/next/approval-rubric.md +30 -12
- package/dist/docs/versioned/next/explanation/README.md +5 -0
- package/dist/docs/versioned/next/explanation/account-linking.md +169 -0
- package/dist/docs/versioned/next/explanation/admin-interface.md +1 -1
- package/dist/docs/versioned/next/explanation/agent-guidelines.md +173 -0
- package/dist/docs/versioned/next/explanation/ai-development-loop.md +196 -0
- package/dist/docs/versioned/next/explanation/authentication.md +45 -1
- package/dist/docs/versioned/next/explanation/consent-forms.md +105 -0
- package/dist/docs/versioned/next/explanation/example-coverage.md +1 -0
- package/dist/docs/versioned/next/explanation/local-first-data.md +1 -1
- package/dist/docs/versioned/next/explanation/organizations.md +37 -0
- package/dist/docs/versioned/next/explanation/planning-plugins.md +4 -0
- package/dist/docs/versioned/next/explanation/responsive-design.md +143 -0
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
- package/dist/docs/versioned/next/explanation/schema-metadata.md +5 -0
- package/dist/docs/versioned/next/how-to/README.md +7 -2
- package/dist/docs/versioned/next/how-to/audit-log.md +22 -3
- package/dist/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
- package/dist/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
- package/dist/docs/versioned/next/how-to/circleci.md +15 -5
- package/dist/docs/versioned/next/how-to/configure-better-auth.md +673 -53
- package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
- package/dist/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
- package/dist/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
- package/dist/docs/versioned/next/how-to/password-reset.md +1 -1
- package/dist/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
- package/dist/docs/versioned/next/how-to/run-tests-locally.md +37 -8
- package/dist/docs/versioned/next/how-to/set-up-social-login.md +424 -0
- package/dist/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
- package/dist/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
- package/dist/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
- package/dist/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
- package/dist/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
- package/dist/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
- package/dist/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
- package/dist/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
- package/dist/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
- package/dist/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
- package/dist/docs/versioned/next/reference/README.md +1 -0
- package/dist/docs/versioned/next/reference/account-security.md +700 -0
- package/dist/docs/versioned/next/reference/admin-backend.md +3 -1
- package/dist/docs/versioned/next/reference/admin-frontend.md +225 -1
- package/dist/docs/versioned/next/reference/api.md +461 -35
- package/dist/docs/versioned/next/reference/cli.md +1 -1
- package/dist/docs/versioned/next/reference/comms.md +8 -0
- package/dist/docs/versioned/next/reference/create-terreno-app.md +3 -1
- package/dist/docs/versioned/next/reference/environment-variables.md +9 -3
- package/dist/docs/versioned/next/reference/legacy/rtk.md +19 -3
- package/dist/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
- package/dist/docs/versioned/next/reference/mcp-server.md +1158 -441
- package/dist/docs/versioned/next/reference/syncdb.md +8 -1
- package/dist/docs/versioned/next/reference/ui.md +1271 -27
- package/dist/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
- package/dist/docs/versioned/next/tutorials/README.md +4 -0
- package/dist/httpApp.d.ts +14 -0
- package/dist/httpApp.d.ts.map +1 -0
- package/dist/httpApp.js +99 -0
- package/dist/httpApp.js.map +1 -0
- package/dist/index.js +8 -90
- package/dist/index.js.map +1 -1
- package/dist/local/localTools.d.ts.map +1 -1
- package/dist/local/localTools.js +44 -14
- package/dist/local/localTools.js.map +1 -1
- package/dist/local/metro/metroDevSession.d.ts +18 -0
- package/dist/local/metro/metroDevSession.d.ts.map +1 -1
- package/dist/local/metro/metroDevSession.js +42 -3
- package/dist/local/metro/metroDevSession.js.map +1 -1
- package/dist/local/tools/databaseQuery.d.ts.map +1 -1
- package/dist/local/tools/databaseQuery.js +3 -13
- package/dist/local/tools/databaseQuery.js.map +1 -1
- package/dist/local/tools/databaseSchema.d.ts.map +1 -1
- package/dist/local/tools/databaseSchema.js +3 -13
- package/dist/local/tools/databaseSchema.js.map +1 -1
- package/dist/local/tools/localMongo.d.ts +10 -0
- package/dist/local/tools/localMongo.d.ts.map +1 -0
- package/dist/local/tools/localMongo.js +30 -0
- package/dist/local/tools/localMongo.js.map +1 -0
- package/dist/local/tools/runtime.d.ts +18 -2
- package/dist/local/tools/runtime.d.ts.map +1 -1
- package/dist/local/tools/runtime.js +75 -20
- package/dist/local/tools/runtime.js.map +1 -1
- package/dist/local/tools/syncDb.d.ts +11 -0
- package/dist/local/tools/syncDb.d.ts.map +1 -1
- package/dist/local/tools/syncDb.js +15 -3
- package/dist/local/tools/syncDb.js.map +1 -1
- package/dist/rateLimit.d.ts +35 -0
- package/dist/rateLimit.d.ts.map +1 -0
- package/dist/rateLimit.js +112 -0
- package/dist/rateLimit.js.map +1 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +42 -60
- package/dist/tools.js.map +1 -1
- package/package.json +4 -3
- package/src/docs/guidelines/ui/guidelines/core.md +1 -1
- package/src/docs/resources/overview.md +4 -2
- package/src/docs/resources/patterns.md +237 -176
- package/src/docs/resources/ui.md +48 -17
- package/src/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
- package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
- package/src/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
- package/src/docs/upgrades/57.20.0.md +238 -0
- package/src/docs/versioned/next/approval-rubric.md +30 -12
- package/src/docs/versioned/next/explanation/README.md +5 -0
- package/src/docs/versioned/next/explanation/account-linking.md +169 -0
- package/src/docs/versioned/next/explanation/admin-interface.md +1 -1
- package/src/docs/versioned/next/explanation/agent-guidelines.md +173 -0
- package/src/docs/versioned/next/explanation/ai-development-loop.md +196 -0
- package/src/docs/versioned/next/explanation/authentication.md +45 -1
- package/src/docs/versioned/next/explanation/consent-forms.md +105 -0
- package/src/docs/versioned/next/explanation/example-coverage.md +1 -0
- package/src/docs/versioned/next/explanation/local-first-data.md +1 -1
- package/src/docs/versioned/next/explanation/organizations.md +37 -0
- package/src/docs/versioned/next/explanation/planning-plugins.md +4 -0
- package/src/docs/versioned/next/explanation/responsive-design.md +143 -0
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
- package/src/docs/versioned/next/explanation/schema-metadata.md +5 -0
- package/src/docs/versioned/next/how-to/README.md +7 -2
- package/src/docs/versioned/next/how-to/audit-log.md +22 -3
- package/src/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
- package/src/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
- package/src/docs/versioned/next/how-to/circleci.md +15 -5
- package/src/docs/versioned/next/how-to/configure-better-auth.md +673 -53
- package/src/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
- package/src/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
- package/src/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
- package/src/docs/versioned/next/how-to/password-reset.md +1 -1
- package/src/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
- package/src/docs/versioned/next/how-to/run-tests-locally.md +37 -8
- package/src/docs/versioned/next/how-to/set-up-social-login.md +424 -0
- package/src/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
- package/src/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
- package/src/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
- package/src/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
- package/src/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
- package/src/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
- package/src/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
- package/src/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
- package/src/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
- package/src/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
- package/src/docs/versioned/next/reference/README.md +1 -0
- package/src/docs/versioned/next/reference/account-security.md +700 -0
- package/src/docs/versioned/next/reference/admin-backend.md +3 -1
- package/src/docs/versioned/next/reference/admin-frontend.md +225 -1
- package/src/docs/versioned/next/reference/api.md +461 -35
- package/src/docs/versioned/next/reference/cli.md +1 -1
- package/src/docs/versioned/next/reference/comms.md +8 -0
- package/src/docs/versioned/next/reference/create-terreno-app.md +3 -1
- package/src/docs/versioned/next/reference/environment-variables.md +9 -3
- package/src/docs/versioned/next/reference/legacy/rtk.md +19 -3
- package/src/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
- package/src/docs/versioned/next/reference/mcp-server.md +1158 -441
- package/src/docs/versioned/next/reference/syncdb.md +8 -1
- package/src/docs/versioned/next/reference/ui.md +1271 -27
- package/src/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
- package/src/docs/versioned/next/tutorials/README.md +4 -0
|
@@ -37,7 +37,7 @@ Optional flags:
|
|
|
37
37
|
| `appName` (positional) | yes | — kebab-case directory and package name (`^[a-z][a-z0-9-]*$`) |
|
|
38
38
|
| `--display-name` | unless `--yes` | derived from `appName` when `--yes` |
|
|
39
39
|
| `--description` | no | empty |
|
|
40
|
-
| `--mcp-server-url` | no | `https://mcp.terreno.app` |
|
|
40
|
+
| `--mcp-server-url` | no | `https://mcp.terreno.app` (base URL; the CLI appends `/mcp`) |
|
|
41
41
|
| `--yes` | no | off |
|
|
42
42
|
|
|
43
43
|
The CLI creates `<cwd>/<appName>/`. Package source: [`create-terreno-app/`](https://github.com/TerrenoLabs/terreno/tree/master/create-terreno-app) in the Terreno monorepo.
|
|
@@ -61,11 +61,11 @@ cd .. && bunx create-terreno-app my-app --yes
|
|
|
61
61
|
| `frontend/` | Expo universal app (`@terreno/ui`, `@terreno/syncdb`, `@terreno/rtk`) |
|
|
62
62
|
| `Dockerfile` | Consumer backend image (`backend/` only — not the Terreno monorepo) |
|
|
63
63
|
| `.dockerignore` | Excludes local env secrets, dependencies, and build output from the image context |
|
|
64
|
-
| `backend/.env`, `frontend/.env` | Gitignored local-dev defaults (written on scaffold) |
|
|
64
|
+
| `backend/.env`, `frontend/.env` | Gitignored local-dev defaults (written on scaffold). `backend/.env` sets `NODE_ENV=development`, which turns on the dev-only `POST /__terreno/browser-logs` route |
|
|
65
65
|
| `backend/.env.example`, `frontend/.env.example` | Committed templates for production and export |
|
|
66
66
|
| `.github/workflows/` | Backend and frontend CI |
|
|
67
|
-
| `.cursor/mcp.json`, `.cursorrules`, `CLAUDE.md` | Agent tooling |
|
|
68
|
-
| `README.md` | App-specific next steps
|
|
67
|
+
| `.cursor/mcp.json`, `.mcp.json`, `.cursorrules`, `CLAUDE.md` | Agent tooling ([Set up Terreno MCP](set-up-terreno-mcp.md)) |
|
|
68
|
+
| `README.md` | App-specific next steps, deploy links, and a link to [Set up Terreno MCP](set-up-terreno-mcp.md) |
|
|
69
69
|
|
|
70
70
|
Generated `@terreno/*` dependencies pin to `^<create-terreno-app version>` at scaffold time.
|
|
71
71
|
|
|
@@ -130,6 +130,8 @@ Open `http://localhost:8082` and sign in as `test@example.com` / `testpassword12
|
|
|
130
130
|
|
|
131
131
|
`frontend/.env` defaults `EXPO_PUBLIC_API_URL` to `http://localhost:4000`. For production web builds, set it **before** export — see [Build for web](build-for-web.md).
|
|
132
132
|
|
|
133
|
+
In development, `frontend/app/_layout.tsx` calls `installTerrenoDevConsoleLogger({baseUrl})` from `@terreno/syncdb` at startup. It sends console errors, warnings, and uncaught errors to the backend's `POST /__terreno/browser-logs`, which writes `backend/.terreno/logs/browser.log`. `terreno-mcp-local` reads that file for `read_logs` and `last_error` with the `browser` source. The call does nothing in production builds. See [Set up Terreno MCP](set-up-terreno-mcp.md).
|
|
134
|
+
|
|
133
135
|
## Deploy
|
|
134
136
|
|
|
135
137
|
Read the [deployment baseline](../explanation/deployment-baseline.md) first.
|
|
@@ -142,10 +144,11 @@ docker run --rm -p 8080:8080 \
|
|
|
142
144
|
-e MONGO_URI='mongodb+srv://...' \
|
|
143
145
|
-e BETTER_AUTH_SECRET='...' \
|
|
144
146
|
-e BETTER_AUTH_URL='https://api.example.com' \
|
|
147
|
+
-e FRONTEND_URL='https://app.example.com' \
|
|
145
148
|
my-app-backend
|
|
146
149
|
```
|
|
147
150
|
|
|
148
|
-
Cloud Run concepts and commands: [Deploy backend to Cloud Run](deploy-backend-to-cloud-run.md). That guide targets Terreno's monorepo example; for a generated app, keep the root `Dockerfile`, port `8080`, and Better Auth variables shown here. The generated backend throws if `BETTER_AUTH_SECRET` is unset. `GET /users` list and read require an admin.
|
|
151
|
+
Cloud Run concepts and commands: [Deploy backend to Cloud Run](deploy-backend-to-cloud-run.md). That guide targets Terreno's monorepo example; for a generated app, keep the root `Dockerfile`, port `8080`, and Better Auth variables shown here. The generated backend throws if `BETTER_AUTH_SECRET` is unset. Set `FRONTEND_URL` to the web app's URL: until it is set, a production backend leaves passkeys off (see [Auth features in generated apps](configure-better-auth.md#auth-features-in-generated-apps)). `GET /users` list and read require an admin.
|
|
149
152
|
|
|
150
153
|
**Frontend:** [Build for web](build-for-web.md) — set `EXPO_PUBLIC_API_URL` at export time.
|
|
151
154
|
|
|
@@ -1,18 +1,378 @@
|
|
|
1
1
|
# Debug a Terreno app with MCP
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Use the local MCP server (`terreno-mcp-local`) to go from a symptom to a verified fix without
|
|
4
|
+
pasting logs into the chat. This guide follows one real failure from start to finish, then lists
|
|
5
|
+
what each tool does. Every tool output below was copied unchanged from a real run against
|
|
6
|
+
`example-backend` and `example-frontend` on web.
|
|
7
|
+
|
|
8
|
+
## Which tool for which symptom
|
|
9
|
+
|
|
10
|
+
| Symptom | Start with | Then |
|
|
11
|
+
|---------|------------|------|
|
|
12
|
+
| API returns 500 | `last_error` with `sources: ["backend"]`. The stack is inside `message`. | `read_logs` with `sources: ["backend"]` and `since` to see the requests around it. |
|
|
13
|
+
| API returns 403 (or another 4xx) | `read_logs` with `sources: ["backend"]` and `level: "warn"`. Every `APIError` under 500 is logged at `warn` as `ForbiddenError(403): <title> — <detail>`, so `last_error` does not show it. | `database_query` to compare the document's `ownerId` with the user in the log line. |
|
|
14
|
+
| App crashes on a screen | Web: `last_error` with `sources: ["browser"]`. An uncaught error carries a separate `stack` field. A render error caught by an error boundary has no `stack`; its `message` names the component instead (see [A screen crashes on web](#a-screen-crashes-on-web)). Native: `last_error` with `sources: ["app"]`. | Native: `navigate` to the screen and call `last_error` again. Web: the `browser` tool. |
|
|
15
|
+
| Data looks wrong | `database_query` to read what the server stored. | `get_client_state` (RTK cache) or `get_syncdb_state` (SyncDB) to compare the client's copy. |
|
|
16
|
+
| Screen renders empty | `get_client_state` or `get_syncdb_state` on native. On web, `browser` with `snapshot`. | `read_logs` with `sources: ["backend"]` to check that the list request ran and returned 200. |
|
|
17
|
+
| Bundler fails | The terminal that runs `expo start`. The `metro` source is always empty under Expo CLI 57: the `/events` socket connects but Expo never sends on it. | On native, `read_logs` with `sources: ["app"]` once the app loads again. |
|
|
18
|
+
|
|
19
|
+
`level` matches one level exactly. `level: "error"` does not include `warn`.
|
|
20
|
+
|
|
21
|
+
## Walkthrough: a malformed request body returns 500
|
|
22
|
+
|
|
23
|
+
The run used the example backend on port 4200 with `NODE_ENV=development`, the web app on port
|
|
24
|
+
8282, and the seeded user `test@example.com`. The MCP server was started with
|
|
25
|
+
`TERRENO_PROJECT_ROOT` set to the repository and `TERRENO_METRO_URL=http://localhost:8282`.
|
|
26
|
+
|
|
27
|
+
### 1. See the symptom
|
|
28
|
+
|
|
29
|
+
A client sent a JSON body with a typo (`fals`). The server answered 500. A malformed body is the
|
|
30
|
+
client's mistake, so it should be a 400 that says what is wrong.
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
$ curl -s -i -b cookies.txt -X POST localhost:4200/todos -H 'Content-Type: application/json' --data '{"title": "Buy milk", "completed": fals}'
|
|
34
|
+
HTTP/1.1 500 Internal Server Error
|
|
35
|
+
X-Powered-By: Express
|
|
36
|
+
X-Request-ID: 5ccfb98c-f592-42ce-9503-415c0c75f32f
|
|
37
|
+
Vary: Origin
|
|
38
|
+
Access-Control-Allow-Credentials: true
|
|
39
|
+
Content-Type: application/json; charset=utf-8
|
|
40
|
+
Content-Length: 46
|
|
41
|
+
Date: Mon, 12 Oct 2026 02:45:16 GMT
|
|
42
|
+
Connection: keep-alive
|
|
43
|
+
Keep-Alive: timeout=5
|
|
44
|
+
|
|
45
|
+
{"status":500,"title":"Internal server error"}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Locate it with `last_error`
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{"name":"last_error","arguments":{"sources":["backend","browser"]}}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{"level":"error","message":"Fallthrough error: SyntaxError: JSON Parse error: Unexpected identifier \"fals\"\nSyntaxError: JSON Parse error: Unexpected identifier \"fals\"\n at <parse> (:0)\n at parse (unknown)\n at parse (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/body-parser/lib/types/json.js:91:21)\n at <anonymous> (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/body-parser/lib/read.js:162:18)\n at runInAsyncScope (node:async_hooks:207:23)\n at invokeCallback (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/raw-body/index.js:238:16)\n at done (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/raw-body/index.js:227:7)\n at onEnd (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/raw-body/index.js:287:7)\n at emit (node:events:97:22)\n at endReadableNT (internal:streams/readable:901:50)\n at processTicksAndRejections (native:7:39)","requestId":"5ccfb98c-f592-42ce-9503-415c0c75f32f","terrenoRequestLog":{"requestId":"5ccfb98c-f592-42ce-9503-415c0c75f32f","userId":null},"timestamp":"2026-10-12T02:45:16.902Z"}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
What this tells you:
|
|
59
|
+
|
|
60
|
+
- `requestId` matches the `X-Request-ID` header from step 1, so this is the same request.
|
|
61
|
+
- `Fallthrough error` comes from `apiFallthroughErrorMiddleware`. That handler only sees errors
|
|
62
|
+
that `apiErrorMiddleware` could not turn into an `APIError`, and it always answers 500.
|
|
63
|
+
- The top frame is `body-parser/lib/types/json.js`. The body failed to parse before any route ran.
|
|
64
|
+
- `userId` is `null` even though the request sent a session cookie. Authentication runs after body
|
|
65
|
+
parsing, so the request never got that far.
|
|
66
|
+
|
|
67
|
+
### 3. Read the surrounding context with `read_logs`
|
|
68
|
+
|
|
69
|
+
Filter to the backend and start from a time just before the failure. A valid request for the same
|
|
70
|
+
route was sent afterwards to compare.
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{"name":"read_logs","arguments":{"sources":["backend"],"since":"2026-10-12T02:45:02.876Z","entries":20}}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"connection": "CDP: not connected; Metro events: not connected",
|
|
79
|
+
"entries": [
|
|
80
|
+
{
|
|
81
|
+
"level": "error",
|
|
82
|
+
"message": "Fallthrough error: SyntaxError: JSON Parse error: Unexpected identifier \"fals\"\nSyntaxError: JSON Parse error: Unexpected identifier \"fals\"\n at <parse> (:0)\n at parse (unknown)\n at parse (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/body-parser/lib/types/json.js:91:21)\n at <anonymous> (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/body-parser/lib/read.js:162:18)\n at runInAsyncScope (node:async_hooks:207:23)\n at invokeCallback (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/raw-body/index.js:238:16)\n at done (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/raw-body/index.js:227:7)\n at onEnd (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/raw-body/index.js:287:7)\n at emit (node:events:97:22)\n at endReadableNT (internal:streams/readable:901:50)\n at processTicksAndRejections (native:7:39)",
|
|
83
|
+
"raw": "{\"level\":\"error\",\"message\":\"Fallthrough error: SyntaxError: JSON Parse error: Unexpected identifier \\\"fals\\\"\\nSyntaxError: JSON Parse error: Unexpected identifier \\\"fals\\\"\\n at <parse> (:0)\\n at parse (unknown)\\n at parse (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/body-parser/lib/types/json.js:91:21)\\n at <anonymous> (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/body-parser/lib/read.js:162:18)\\n at runInAsyncScope (node:async_hooks:207:23)\\n at invokeCallback (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/raw-body/index.js:238:16)\\n at done (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/raw-body/index.js:227:7)\\n at onEnd (/workspace/.claude/worktrees/agent-a4ec9221d12c64054/node_modules/raw-body/index.js:287:7)\\n at emit (node:events:97:22)\\n at endReadableNT (internal:streams/readable:901:50)\\n at processTicksAndRejections (native:7:39)\",\"requestId\":\"5ccfb98c-f592-42ce-9503-415c0c75f32f\",\"terrenoRequestLog\":{\"requestId\":\"5ccfb98c-f592-42ce-9503-415c0c75f32f\",\"userId\":null},\"timestamp\":\"2026-10-12T02:45:16.902Z\"}",
|
|
84
|
+
"source": "backend",
|
|
85
|
+
"timestamp": "2026-10-12T02:45:16.902Z"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"level": "debug",
|
|
89
|
+
"message": "POST <- /todos <User:6acc4989b98fcbeba02df577> Body: {\"title\":\"Buy milk\",\"completed\":false}",
|
|
90
|
+
"raw": "{\"level\":\"debug\",\"message\":\"POST <- /todos <User:6acc4989b98fcbeba02df577> Body: {\\\"title\\\":\\\"Buy milk\\\",\\\"completed\\\":false}\",\"requestId\":\"91b0a231-17d7-42ef-8794-570410c99fc7\",\"sessionId\":\"6acc49aed7ff6fef9a4a80c9\",\"terrenoRequestLog\":{\"requestId\":\"91b0a231-17d7-42ef-8794-570410c99fc7\",\"userId\":\"6acc4989b98fcbeba02df577\"},\"timestamp\":\"2026-10-12T02:45:26.017Z\",\"userId\":\"6acc4989b98fcbeba02df577\"}",
|
|
91
|
+
"source": "backend",
|
|
92
|
+
"timestamp": "2026-10-12T02:45:26.017Z"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"level": "info",
|
|
96
|
+
"message": "[comms:mail] recipients=1 subjectLength=10",
|
|
97
|
+
"raw": "{\"level\":\"info\",\"message\":\"[comms:mail] recipients=1 subjectLength=10\",\"requestId\":\"91b0a231-17d7-42ef-8794-570410c99fc7\",\"sessionId\":\"6acc49aed7ff6fef9a4a80c9\",\"terrenoRequestLog\":{\"requestId\":\"91b0a231-17d7-42ef-8794-570410c99fc7\",\"userId\":\"6acc4989b98fcbeba02df577\"},\"timestamp\":\"2026-10-12T02:45:26.101Z\",\"userId\":\"6acc4989b98fcbeba02df577\"}",
|
|
98
|
+
"source": "backend",
|
|
99
|
+
"timestamp": "2026-10-12T02:45:26.101Z"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"level": "debug",
|
|
103
|
+
"message": "POST -> /todos 201 108.791011ms",
|
|
104
|
+
"raw": "{\"level\":\"debug\",\"message\":\"POST -> /todos 201 108.791011ms\",\"requestId\":\"91b0a231-17d7-42ef-8794-570410c99fc7\",\"sessionId\":\"6acc49aed7ff6fef9a4a80c9\",\"terrenoRequestLog\":{\"requestId\":\"91b0a231-17d7-42ef-8794-570410c99fc7\",\"userId\":\"6acc4989b98fcbeba02df577\"},\"timestamp\":\"2026-10-12T02:45:26.126Z\",\"userId\":\"6acc4989b98fcbeba02df577\"}",
|
|
105
|
+
"source": "backend",
|
|
106
|
+
"timestamp": "2026-10-12T02:45:26.126Z"
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"projectRoot": "/workspace/.claude/worktrees/agent-a4ec9221d12c64054"
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The valid request logs `POST <- /todos` with the user and `POST -> /todos 201`. The malformed one
|
|
114
|
+
has neither line, only the error. That confirms it never reached request logging, auth, or the
|
|
115
|
+
`/todos` router. The fix belongs in the app-level error handling, not in the todo route.
|
|
116
|
+
|
|
117
|
+
`raw` is the log line exactly as the backend wrote it. `connection` reports the Metro and Hermes
|
|
118
|
+
sources; it does not affect `backend` entries.
|
|
119
|
+
|
|
120
|
+
### 4. Check the data with `database_query`
|
|
121
|
+
|
|
122
|
+
Did the failed request write anything?
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{"name":"database_query","arguments":{"collection":"todos","operation":"find","filter":{"title":"Buy milk"},"limit":5}}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"collection": "todos",
|
|
131
|
+
"count": 1,
|
|
132
|
+
"rows": [
|
|
133
|
+
{
|
|
134
|
+
"_id": "6acc49c6d7ff6fef9a4a80ca",
|
|
135
|
+
"completed": false,
|
|
136
|
+
"ownerId": "6acc4989b98fcbeba02df577",
|
|
137
|
+
"tags": [],
|
|
138
|
+
"title": "Buy milk",
|
|
139
|
+
"updated": "2026-10-12T02:45:26.060Z",
|
|
140
|
+
"created": "2026-10-12T02:45:26.060Z",
|
|
141
|
+
"deleted": false,
|
|
142
|
+
"_syncPrevStream": null,
|
|
143
|
+
"_syncSeq": 3,
|
|
144
|
+
"__v": 0
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
One row, created at `02:45:26` by the valid request. The malformed request at `02:45:16` wrote
|
|
151
|
+
nothing, so there is no bad data to clean up.
|
|
152
|
+
|
|
153
|
+
### 5. Check the client with `get_client_state`
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{"name":"get_client_state","arguments":{"slice":"rtk"}}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
On web this returns `layer: "none"`:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"connection": "CDP: Error: No debuggable Hermes/React Native target with webSocketDebuggerUrl (is the app running?); Metro events: not connected",
|
|
164
|
+
"hint": "No client data layer found. For RTK Query, call `registerTerrenoDevStore(store)` from @terreno/rtk in __DEV__. For SyncDB, create the client with `createSyncDb({debug: true})`. Keep the app running; when the MCP process cannot see the app heap, start Metro so CDP can reach Hermes (see `connection`).",
|
|
165
|
+
"layer": "none",
|
|
166
|
+
"reasons": {
|
|
167
|
+
"rtk": "Error: No debuggable Hermes/React Native target with webSocketDebuggerUrl (is the app running?)",
|
|
168
|
+
"syncdb": "Error: Error: No debuggable Hermes/React Native target with webSocketDebuggerUrl (is the app running?)\nCDP: Error: No debuggable Hermes/React Native target with webSocketDebuggerUrl (is the app running?); Metro events: not connected"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`get_client_state` reads the client through Hermes over CDP. A web page runs in the browser, not
|
|
174
|
+
Hermes, so there is no target even though the store is registered. To check client state on web,
|
|
175
|
+
use the `browser` tool (step 8). On iOS or Android, run a development build attached to Metro and
|
|
176
|
+
the same call returns the RTK or SyncDB state.
|
|
5
177
|
|
|
6
|
-
|
|
178
|
+
This failure is server-side, so the client check only confirms there is nothing to fix there.
|
|
7
179
|
|
|
8
|
-
|
|
180
|
+
### 6. Fix it
|
|
181
|
+
|
|
182
|
+
`apiErrorMiddleware` already converts Mongoose validation and cast errors to `APIError`s. It now
|
|
183
|
+
also converts body-parser client errors through a new `bodyParserErrorToAPIError` helper in
|
|
184
|
+
`api/src/errors.ts`:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const apiError = isAPIError(err)
|
|
188
|
+
? err
|
|
189
|
+
: (mongooseErrorToAPIError(err) ?? bodyParserErrorToAPIError(err));
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The helper keeps the parser's own 4xx status and message. A malformed body becomes 400
|
|
193
|
+
`Malformed request body`, an oversized body 413 `Request body too large`, and an unsupported
|
|
194
|
+
charset 415 `Unsupported request body encoding`. Errors body-parser marks as server faults still
|
|
195
|
+
fall through to 500. Regression tests in `api/src/errors.test.ts` build an app with the same
|
|
196
|
+
middleware order as `TerrenoApp` and fail without the change:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
cd api && bun test src/errors.test.ts
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Rebuild `@terreno/api` (`bun run compile`) and restart the backend. Check that the old process
|
|
203
|
+
has exited first. In this run, the first restart printed
|
|
204
|
+
`Server start failed: Error: Failed to start server. Is port 4200 in use?` and the old server kept
|
|
205
|
+
answering 500.
|
|
206
|
+
|
|
207
|
+
### 7. Re-run the symptom
|
|
208
|
+
|
|
209
|
+
Same request against the restarted backend:
|
|
210
|
+
|
|
211
|
+
```text
|
|
212
|
+
$ curl -s -i -b cookies.txt -X POST localhost:4200/todos -H 'Content-Type: application/json' --data '{"title": "Buy milk", "completed": fals}'
|
|
213
|
+
HTTP/1.1 400 Bad Request
|
|
214
|
+
X-Powered-By: Express
|
|
215
|
+
X-Request-ID: b81e4036-1243-4b0d-a3b4-9e2ec9807664
|
|
216
|
+
Vary: Origin
|
|
217
|
+
Access-Control-Allow-Credentials: true
|
|
218
|
+
Content-Type: application/json; charset=utf-8
|
|
219
|
+
Content-Length: 143
|
|
220
|
+
Date: Mon, 12 Oct 2026 02:47:11 GMT
|
|
221
|
+
Connection: keep-alive
|
|
222
|
+
Keep-Alive: timeout=5
|
|
223
|
+
|
|
224
|
+
{"status":400,"title":"Malformed request body","detail":"JSON Parse error: Unexpected identifier \"fals\"","disableExternalErrorTracking":true}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`last_error` has no `since` argument, so it keeps returning the newest error even after a fix. To
|
|
228
|
+
prove nothing new failed, ask `read_logs` for error-level backend entries since the restart:
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{"name":"read_logs","arguments":{"sources":["backend"],"level":"error","since":"2026-10-12T02:47:10.183Z"}}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"connection": "CDP: Error: No debuggable Hermes/React Native target with webSocketDebuggerUrl (is the app running?); Metro events: connected ws://localhost:8282/events",
|
|
237
|
+
"entries": [],
|
|
238
|
+
"projectRoot": "/workspace/.claude/worktrees/agent-a4ec9221d12c64054"
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The request is now logged at `warn` as
|
|
243
|
+
`BadRequestError(400): Malformed request body — JSON Parse error: Unexpected identifier "fals"`,
|
|
244
|
+
followed by the parser's stack. Query with `level: "warn"` to see it.
|
|
245
|
+
|
|
246
|
+
### 8. Confirm in the running app
|
|
247
|
+
|
|
248
|
+
`navigate` drives Expo Router through Hermes, so it has no target on web:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{"name":"navigate","arguments":{"path":"/"}}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"error": "Error: No debuggable Hermes/React Native target with webSocketDebuggerUrl (is the app running?)",
|
|
257
|
+
"status": "CDP: Error: No debuggable Hermes/React Native target with webSocketDebuggerUrl (is the app running?); Metro events: not connected"
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
On web, use the local `browser` tool instead (it needs `TERRENO_MCP_EVAL=1` for `evaluate`). Open
|
|
262
|
+
the app and log in:
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
{"name":"browser","arguments":{"action":"open","url":"http://localhost:8282/login","width":1280,"height":800}}
|
|
266
|
+
{"name":"browser","arguments":{"action":"type","selector":"#login-screen-email-input","text":"test@example.com"}}
|
|
267
|
+
{"name":"browser","arguments":{"action":"type","selector":"#login-screen-password-input","text":"testpassword123"}}
|
|
268
|
+
{"name":"browser","arguments":{"action":"click","selector":"[data-testid=\"login-screen-submit-button\"]"}}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Each step returns the page state, for example the click:
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"action": "click",
|
|
276
|
+
"ok": true,
|
|
277
|
+
"selector": "[data-testid=\"login-screen-submit-button\"]",
|
|
278
|
+
"title": "",
|
|
279
|
+
"url": "http://localhost:8282/login"
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
On a user's first login, the example app shows one-time Terms of Service and Privacy Policy
|
|
284
|
+
consent forms before the Todos screen. The Terms form needs a drawn signature, which `browser`
|
|
285
|
+
cannot draw. For a scripted run, sign in with `POST /api/auth/sign-in/email`, list the forms with
|
|
286
|
+
`GET /consents/pending`, and accept each one with `POST /consents/respond` and the body
|
|
287
|
+
`{"consentFormId": "<id>", "agreed": true, "locale": "en", "signature": "<data URL>"}`. Then
|
|
288
|
+
`reload` the page.
|
|
289
|
+
|
|
290
|
+
Send the same malformed body from the page, the way a web client would:
|
|
291
|
+
|
|
292
|
+
```json
|
|
293
|
+
{"name":"browser","arguments":{"action":"evaluate","code":"fetch(\"http://localhost:4200/todos\", {method: \"POST\", credentials: \"include\", headers: {\"Content-Type\": \"application/json\"}, body: \"{\\\"title\\\": \\\"Buy milk\\\", \\\"completed\\\": fals}\"}).then(async (r) => ({status: r.status, body: await r.json()}))"}}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
```json
|
|
297
|
+
{
|
|
298
|
+
"action": "evaluate",
|
|
299
|
+
"ok": true,
|
|
300
|
+
"result": {
|
|
301
|
+
"status": 400,
|
|
302
|
+
"body": {
|
|
303
|
+
"status": 400,
|
|
304
|
+
"title": "Malformed request body",
|
|
305
|
+
"detail": "JSON Parse error: Unexpected identifier \"fals\"",
|
|
306
|
+
"disableExternalErrorTracking": true
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
"title": "",
|
|
310
|
+
"url": "http://localhost:8282/"
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Then take a screenshot of the todo list:
|
|
315
|
+
|
|
316
|
+
```json
|
|
317
|
+
{"name":"browser","arguments":{"action":"screenshot","output":".terreno/pipeline/screens/todos-after-fix.png"}}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
{
|
|
322
|
+
"action": "screenshot",
|
|
323
|
+
"ok": true,
|
|
324
|
+
"output": "/workspace/.claude/worktrees/agent-a4ec9221d12c64054/.terreno/pipeline/screens/todos-after-fix.png",
|
|
325
|
+
"title": "",
|
|
326
|
+
"url": "http://localhost:8282/"
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The screenshot shows My Todos with `3 todos · 3 remaining · 0 completed`: the seeded todos plus the
|
|
331
|
+
single `Buy milk`, with no extra row from the rejected requests. A final `read_logs` for
|
|
332
|
+
backend errors since the restart still returned `"entries": []`. The fix is verified.
|
|
333
|
+
|
|
334
|
+
## A screen crashes on web
|
|
335
|
+
|
|
336
|
+
A second run used the same setup on ports 4400 and 8482. A new priority badge on each todo row
|
|
337
|
+
read `data.priority.toUpperCase()`, and todos created before the field existed have no
|
|
338
|
+
`priority`. The Todos screen showed `Something went wrong` with
|
|
339
|
+
`Cannot read properties of undefined (reading 'toUpperCase')`.
|
|
340
|
+
|
|
341
|
+
```json
|
|
342
|
+
{"name":"last_error","arguments":{"sources":["backend","browser"]}}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
{"level":"error","message":"%o\n\n%s\n\n%s\n TypeError: Cannot read properties of undefined (reading 'toUpperCase') The above error occurred in the <SyncTodoItem> component. React will try to recreate this component tree from scratch using the error boundary you provided, Try.","source":"browser","timestamp":"2026-10-12T03:30:28.321+00:00"}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
React caught the error in an error boundary and reported it with `console.error`, so:
|
|
350
|
+
|
|
351
|
+
- There is no `stack` field. Only uncaught errors carry one.
|
|
352
|
+
- The message starts with React's unformatted `%o` and `%s` placeholders. Read past them.
|
|
353
|
+
- The component is named: `<SyncTodoItem>`. Search the code for it and for the property in the
|
|
354
|
+
error, here `toUpperCase`.
|
|
355
|
+
|
|
356
|
+
`database_query` with `{"priority": {"$exists": false}}` on `todos` returned three rows, which
|
|
357
|
+
confirmed that the data, not the server, triggers the crash. `read_logs` with
|
|
358
|
+
`sources: ["backend"]` and `level: "error"` returned no entries. Restoring the guard
|
|
359
|
+
(`data.priority ? <Badge …/> : null`) fixed it. `navigate` returned the same
|
|
360
|
+
`No debuggable Hermes/React Native target` error as in step 8, so the check used `browser`:
|
|
361
|
+
`open`, `scroll` to a todo row, then `screenshot`. The row with a priority showed its badge, and the
|
|
362
|
+
rows without one rendered.
|
|
363
|
+
|
|
364
|
+
A later `last_error` may return an unrelated older error, because it has no `since`. Use
|
|
365
|
+
`read_logs` with `level: "error"` and `since` set to the time of the fix instead.
|
|
366
|
+
|
|
367
|
+
## Tool reference
|
|
368
|
+
|
|
369
|
+
### Read errors and logs
|
|
9
370
|
|
|
10
371
|
```json
|
|
11
372
|
{"name":"last_error","arguments":{"sources":["backend","browser","metro","app"]}}
|
|
12
373
|
```
|
|
13
374
|
|
|
14
|
-
|
|
15
|
-
events:
|
|
375
|
+
`last_error` returns the newest error-level entry from the selected sources.
|
|
16
376
|
|
|
17
377
|
```json
|
|
18
378
|
{
|
|
@@ -20,14 +380,15 @@ events:
|
|
|
20
380
|
"arguments": {
|
|
21
381
|
"entries": 100,
|
|
22
382
|
"level": "error",
|
|
383
|
+
"since": "2026-10-12T02:45:02.876Z",
|
|
23
384
|
"sources": ["backend", "browser", "metro", "app"]
|
|
24
385
|
}
|
|
25
386
|
}
|
|
26
387
|
```
|
|
27
388
|
|
|
28
|
-
`backend` and `browser` read durable JSONL
|
|
29
|
-
Metro `/events` and CDP.
|
|
30
|
-
unavailable.
|
|
389
|
+
`backend` and `browser` read durable JSONL files under `.terreno/logs/`. `metro` and `app` read
|
|
390
|
+
in-memory rings filled from Metro `/events` and Hermes CDP. `connection` explains when a live
|
|
391
|
+
source is unavailable.
|
|
31
392
|
|
|
32
393
|
CLI equivalents:
|
|
33
394
|
|
|
@@ -36,26 +397,31 @@ terreno logs last-error --sources backend,browser,metro,app
|
|
|
36
397
|
terreno logs --entries 100 --level error --sources backend,browser,metro,app
|
|
37
398
|
```
|
|
38
399
|
|
|
39
|
-
|
|
400
|
+
### Inspect client state
|
|
40
401
|
|
|
41
402
|
Check authentication and non-synced RTK Query cache state:
|
|
42
403
|
|
|
43
404
|
```json
|
|
44
|
-
{"name":"
|
|
405
|
+
{"name":"get_client_state","arguments":{"slice":"rtk","query":"todos"}}
|
|
45
406
|
```
|
|
46
407
|
|
|
47
408
|
The tool reads `globalThis.__TERRENO_STORE__` registered by the app, using CDP when the MCP process
|
|
48
|
-
cannot access the app heap directly.
|
|
49
|
-
|
|
409
|
+
cannot access the app heap directly. It also detects a SyncDB client created with `debug: true`, and
|
|
410
|
+
its `layer` field reports what it found: `rtk`, `syncdb`, `syncdb+rtk` (both payloads returned), or
|
|
411
|
+
`none` (with a setup hint). On web it returns `none`, as shown in step 5. `get_rtk_state` is a
|
|
412
|
+
deprecated alias for one release. `slice: "auth"` prefers the Better Auth slice and falls back to
|
|
413
|
+
legacy JWT auth; pass `betterAuth` to request that exact slice.
|
|
50
414
|
|
|
51
415
|
Sensitive fields whose names include `password`, `token`, `secret`, `authorization`, or `cookie` are
|
|
52
416
|
returned as `[REDACTED]`; use this summary to inspect cache state, never to retrieve credentials.
|
|
53
417
|
|
|
418
|
+
The CLI equivalent is:
|
|
419
|
+
|
|
54
420
|
```bash
|
|
55
421
|
terreno state --slice rtk --query todos
|
|
56
422
|
```
|
|
57
423
|
|
|
58
|
-
|
|
424
|
+
### Inspect and change SyncDB state
|
|
59
425
|
|
|
60
426
|
Create the app client with `debug: true`, then inspect the same state that powers
|
|
61
427
|
the SyncDB debugger:
|
|
@@ -85,7 +451,7 @@ use `setLocalEntity` from `syncdb_snapshot` `get`.
|
|
|
85
451
|
State changes require an explicit local opt-in:
|
|
86
452
|
|
|
87
453
|
```bash
|
|
88
|
-
TERRENO_MCP_EVAL=1 bunx terreno-mcp-local
|
|
454
|
+
TERRENO_MCP_EVAL=1 bunx -p @terreno/mcp terreno-mcp-local
|
|
89
455
|
```
|
|
90
456
|
|
|
91
457
|
Then use `syncdb_action`. Normal writes should use `mutate`; `flush` drains the
|
|
@@ -104,9 +470,9 @@ semantics. `mergeSnapshot` CRDT-merges the captured TinyBase content into the
|
|
|
104
470
|
live store (later HLCs win) instead of replacing it. Capture a baseline first
|
|
105
471
|
and use these only for fault injection or repair.
|
|
106
472
|
|
|
107
|
-
|
|
473
|
+
### Drive the web app
|
|
108
474
|
|
|
109
|
-
|
|
475
|
+
The local `browser` tool uses Bun 1.4's built-in WebView:
|
|
110
476
|
|
|
111
477
|
```json
|
|
112
478
|
{"name":"browser","arguments":{"action":"open","url":"http://localhost:8082"}}
|
|
@@ -118,8 +484,10 @@ Use Bun 1.4's built-in WebView through the local `browser` tool:
|
|
|
118
484
|
```
|
|
119
485
|
|
|
120
486
|
The session persists between MCP calls. `snapshot` returns visible text and up to 200 interactive
|
|
121
|
-
elements so the agent can choose selectors and verify state without image guessing. `
|
|
122
|
-
|
|
487
|
+
elements so the agent can choose selectors and verify state without image guessing. `wait` sleeps
|
|
488
|
+
for `timeout` milliseconds; it does not wait for a selector. `screenshot` saves the viewport, and
|
|
489
|
+
its `output` must be under the project root or `/opt/cursor/artifacts`. `evaluate` requires
|
|
490
|
+
`TERRENO_MCP_EVAL=1`.
|
|
123
491
|
WebView console output is not added to `browser.log`; call `read_logs` alongside this flow when the
|
|
124
492
|
running app posts browser logs to the backend collector. The collector accepts local-loopback
|
|
125
493
|
traffic before login; devices or remote browsers must send the app's normal authentication.
|
|
@@ -133,12 +501,12 @@ terreno web http://localhost:8082 \
|
|
|
133
501
|
--screenshot /opt/cursor/artifacts/save-result.png
|
|
134
502
|
```
|
|
135
503
|
|
|
136
|
-
|
|
504
|
+
### Verify a native fix in the running app
|
|
137
505
|
|
|
138
506
|
Navigation and arbitrary evaluation are disabled by default. Opt in only for local debugging:
|
|
139
507
|
|
|
140
508
|
```bash
|
|
141
|
-
TERRENO_MCP_EVAL=1 bunx terreno-mcp-local
|
|
509
|
+
TERRENO_MCP_EVAL=1 bunx -p @terreno/mcp terreno-mcp-local
|
|
142
510
|
```
|
|
143
511
|
|
|
144
512
|
Then call `navigate` with `{"path":"/profile"}` or run:
|
|
@@ -147,8 +515,8 @@ Then call `navigate` with `{"path":"/profile"}` or run:
|
|
|
147
515
|
TERRENO_MCP_EVAL=1 terreno navigate /profile
|
|
148
516
|
```
|
|
149
517
|
|
|
150
|
-
Confirm the target screen renders, then call `
|
|
151
|
-
|
|
518
|
+
Confirm the target screen renders, then call `read_logs` with `level: "error"` and `since` set to
|
|
519
|
+
the navigation time. A successful fix returns no entries.
|
|
152
520
|
|
|
153
521
|
## Connection problems
|
|
154
522
|
|
|
@@ -158,3 +526,5 @@ error-level entry after the navigation timestamp.
|
|
|
158
526
|
- Set `TERRENO_METRO_URL` if Metro is not on the frontend script's `--port`.
|
|
159
527
|
- Close React Native DevTools when Hermes reports another debugger connection.
|
|
160
528
|
- Use only `backend,browser` sources when Metro is intentionally stopped.
|
|
529
|
+
- Expect `No debuggable Hermes/React Native target` from `get_client_state`, `navigate`, and
|
|
530
|
+
`evaluate` on web. Those tools need an iOS or Android build attached to Metro.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Enable MCP OAuth for a JWT app
|
|
2
2
|
|
|
3
|
-
Let Claude.ai custom connectors, ChatGPT apps, and other remote MCP clients connect to your app's `POST /mcp` through OAuth 2.1
|
|
3
|
+
Let Claude.ai custom connectors, ChatGPT apps, and other remote MCP clients connect to your app's `POST /mcp` through OAuth 2.1. Terreno runs the authorization server; your app hosts one consent screen. A Terreno JWT is the usual way to approve that screen. An app with its own session sets `req.user` first. See [Apps without a Terreno JWT](#apps-without-a-terreno-jwt).
|
|
4
4
|
|
|
5
|
-
Use [a service token](connect-mcp-service-token.md)
|
|
5
|
+
Use [a service token](connect-mcp-service-token.md) when the client can only hold a static Bearer key (Perplexity, Cursor, Claude Code). For a headless client that can perform a token request, use [client credentials](#headless-clients-client-credentials) instead.
|
|
6
6
|
|
|
7
7
|
## Prerequisites
|
|
8
8
|
|
|
@@ -46,6 +46,8 @@ Dynamic client registration (`POST /oauth/register`) is a separate opt-in and st
|
|
|
46
46
|
});
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
A loopback redirect (`localhost`, `127.0.0.1`, or `[::1]`) matches any port. Register one URI, for example `http://127.0.0.1:33418/callback`, and a native client can send `http://127.0.0.1:<ephemeral>/callback` with the same scheme, host, path, and query. The authorization code is sent to the port the client used. `localhost` and `127.0.0.1` do not match each other. Every other redirect URI must match exactly. There is no per-client opt-out. Another process on that host can listen on a different port and receive the code. PKCE `S256` is what stops that process from exchanging it.
|
|
50
|
+
|
|
49
51
|
- **Registration on.** Turn it on only when clients you cannot pre-register must connect. Set `dynamicClientRegistration: true` and list `allowedRedirectHosts`, which limits where a registered client may send codes. Claude.ai redirects to `claude.ai`, ChatGPT to `chatgpt.com`. Use `*.example.com` for subdomains, and add `localhost` only if you want local clients with loopback `http://` redirects. Keep rate limiting on.
|
|
50
52
|
|
|
51
53
|
```typescript
|
|
@@ -88,6 +90,29 @@ The client sends the user's browser to `/oauth/authorize`. Terreno checks the cl
|
|
|
88
90
|
|
|
89
91
|
The three endpoints are in OpenAPI (`getMcpOAuthRequest`, `approveMcpOAuthRequest`, `denyMcpOAuthRequest`), so `bun run sdk` generates hooks for them. A request expires after 10 minutes (400 `Authorization request expired`), and a decided request cannot be decided again (409). The endpoints refuse `mcp_` and OAuth bearers, so an agent cannot approve itself.
|
|
90
92
|
|
|
93
|
+
### Apps without a Terreno JWT
|
|
94
|
+
|
|
95
|
+
`authenticateMiddleware()` calls `next()` when `req.user` is already set, so the consent routes do not require a JWT or a Better Auth session. Set the approving user in `addMiddleware`, which runs before these routes. Limit that to the consent paths. `GET /oauth/authorize` stays the unauthenticated redirect that starts the flow.
|
|
96
|
+
|
|
97
|
+
The user object needs `_id`. Approve issues the grant to that id and records that user's `tokenEpoch`. `userForThisRequest` must resolve a session the app has already verified, such as a signed session cookie. Do not set `req.user` from an unverified header or query parameter. That user becomes the subject of the grant.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
new TerrenoApp({userModel: User, mcpOAuth: {consentUrl, enabled: true, issuer}})
|
|
101
|
+
.addMiddleware(async (req, _res, next) => {
|
|
102
|
+
if (!req.path.startsWith("/oauth/authorize/")) {
|
|
103
|
+
next();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const user = await userForThisRequest(req);
|
|
107
|
+
if (user) {
|
|
108
|
+
req.user = user;
|
|
109
|
+
}
|
|
110
|
+
next();
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Call `GET /oauth/authorize/<request>` and `POST .../approve` or `.../deny` with no `Authorization` header. `mcp_` and `mcpat_` bearers are still rejected on those routes.
|
|
115
|
+
|
|
91
116
|
## 3. Connect a client
|
|
92
117
|
|
|
93
118
|
Give the client the MCP URL, `https://api.example.com/mcp`. It discovers the rest:
|
|
@@ -98,6 +123,32 @@ Give the client the MCP URL, `https://api.example.com/mcp`. It discovers the res
|
|
|
98
123
|
|
|
99
124
|
For Claude.ai: **Settings → Connectors → Add custom connector**, paste the MCP URL, and connect. For ChatGPT: create a connector with the MCP URL and choose OAuth.
|
|
100
125
|
|
|
126
|
+
## Headless clients (client credentials)
|
|
127
|
+
|
|
128
|
+
A sandbox or another server has no browser, so it cannot run the authorization-code flow. Create a confidential client bound to a service-account user. The helper returns the secret once. Store it, then exchange it at `POST /oauth/token`. Dynamic registration stays public-only and still rejects client secrets.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import {McpOAuthClient} from "@terreno/api";
|
|
132
|
+
|
|
133
|
+
const {client, clientSecret} = await McpOAuthClient.issueConfidential({
|
|
134
|
+
clientName: "Sandbox agent",
|
|
135
|
+
tokenEndpointAuthMethod: "client_secret_basic", // or "client_secret_post"
|
|
136
|
+
userId: serviceAccount._id,
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`client_secret_basic` sends `Authorization: Basic` with the form-encoded `client_id:client_secret`. `client_secret_post` sends `client_id` and `client_secret` in the form body. The client must use the method it was created with.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Form-encode the id and secret before base64. A raw colon join breaks when either value contains '%' or ':'.
|
|
144
|
+
AUTH=$(node -e 'const [id, secret] = process.argv.slice(1); process.stdout.write(Buffer.from(`${encodeURIComponent(id)}:${encodeURIComponent(secret)}`).toString("base64"))' "$CLIENT_ID" "$CLIENT_SECRET")
|
|
145
|
+
curl -X POST https://api.example.com/oauth/token \
|
|
146
|
+
-H "Authorization: Basic $AUTH" \
|
|
147
|
+
-d grant_type=client_credentials
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The response is an `mcpat_…` access token and no refresh token. It acts as `userId`, with `credential: "oauth"`. Disabling that user, or bumping `tokenEpoch`, ends the token the same way it ends an authorization-code token. `McpOAuthClient.revoke(clientId)` ends every token that client holds, and a revoked client cannot start a new authorization or token grant. `deleteOne`, `findOneAndDelete`, and `deleteMany` do the same. `bulkWrite` skips those hooks, so use `revoke` when a bulk write removes clients.
|
|
151
|
+
|
|
101
152
|
## 4. Check it
|
|
102
153
|
|
|
103
154
|
```bash
|
|
@@ -128,7 +179,7 @@ Refresh tokens rotate on every use. If an old refresh token comes back, Terreno
|
|
|
128
179
|
|
|
129
180
|
## Security notes
|
|
130
181
|
|
|
131
|
-
-
|
|
182
|
+
- Public clients use PKCE `S256` only. `plain` PKCE is rejected. Confidential clients use `client_credentials` with `client_secret_basic` or `client_secret_post`. `private_key_jwt` is not supported. Dynamic registration cannot create a confidential client.
|
|
132
183
|
- Tokens, codes, and refresh tokens are stored as SHA-256 hashes.
|
|
133
184
|
- Registration, authorize, token, and revoke count against the `auth` [rate-limit](rate-limiting.md) bucket. Turn rate limiting on in production, since registration is unauthenticated.
|
|
134
185
|
- If a browser-based client (such as the MCP Inspector) runs discovery from another origin, allow that origin in `corsOrigin`.
|