@noodleseed/agent-kit 0.12.0 → 0.14.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/README.md +3 -3
- package/manifest.json +39 -9
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +7 -2
- package/skills/claude-code/references/authoring-workflow.md +94 -49
- package/skills/claude-code/references/chatgpt-compliance.md +63 -0
- package/skills/claude-code/references/connect-an-api.md +76 -0
- package/skills/claude-code/references/examples.md +4 -1
- package/skills/claude-code/references/experience-design.md +142 -0
- package/skills/claude-code/references/troubleshooting.md +1 -1
- package/skills/codex/SKILL.md +7 -2
- package/skills/codex/references/authoring-workflow.md +94 -49
- package/skills/codex/references/chatgpt-compliance.md +63 -0
- package/skills/codex/references/connect-an-api.md +76 -0
- package/skills/codex/references/examples.md +4 -1
- package/skills/codex/references/experience-design.md +142 -0
- package/skills/codex/references/troubleshooting.md +1 -1
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ into a project's `.agents/` and `.claude/` skill directories via `noodle agents
|
|
|
8
8
|
|
|
9
9
|
## Contents
|
|
10
10
|
|
|
11
|
-
- `skills/codex/` — the Codex (Codex CLI / AGENTS.md) skill: a `SKILL.md` router plus
|
|
11
|
+
- `skills/codex/` — the Codex (Codex CLI / AGENTS.md) skill: a `SKILL.md` router plus fourteen
|
|
12
12
|
`references/*.md` files (sdk-surface, cli-commands, agent-contract, compile-errors,
|
|
13
|
-
authoring-workflow, widgets-and-apps, test-in-hosts,
|
|
14
|
-
examples).
|
|
13
|
+
authoring-workflow, connect-an-api, experience-design, widgets-and-apps, test-in-hosts,
|
|
14
|
+
troubleshooting, deploy-and-ops, publishing, chatgpt-compliance, examples).
|
|
15
15
|
- `skills/claude-code/` — the Claude Code skill, same hierarchical tree.
|
|
16
16
|
- `manifest.json` — `{ packageVersion, files: [{ path, sha256, agentTarget }] }`. The CLI verifies every
|
|
17
17
|
file's sha256 against this manifest before writing it into a user's project; a mismatch refuses the
|
package/manifest.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"packageVersion": "0.
|
|
2
|
+
"packageVersion": "0.14.0",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": "skills/codex/SKILL.md",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "430e32c03fd13e62cc285280ed846178ae941821516445ba210e5a77fb9e49cd",
|
|
7
7
|
"agentTarget": "codex"
|
|
8
8
|
},
|
|
9
9
|
{
|
|
@@ -28,7 +28,17 @@
|
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "skills/codex/references/authoring-workflow.md",
|
|
31
|
-
"sha256": "
|
|
31
|
+
"sha256": "eca8d1f2b98ee64f6beda4d983f0f0df9023ceed8bd00ffb9dd6b5025dcaa4e4",
|
|
32
|
+
"agentTarget": "codex"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"path": "skills/codex/references/connect-an-api.md",
|
|
36
|
+
"sha256": "5c7d763ad57e6d6b67e3e115da48acd5bef3687acd050b776416906076cec440",
|
|
37
|
+
"agentTarget": "codex"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"path": "skills/codex/references/experience-design.md",
|
|
41
|
+
"sha256": "f07ed621eb1e8cdfa08abd10d5a6331c7e9d8198da422b8b4ad54d23475b2d4d",
|
|
32
42
|
"agentTarget": "codex"
|
|
33
43
|
},
|
|
34
44
|
{
|
|
@@ -43,7 +53,7 @@
|
|
|
43
53
|
},
|
|
44
54
|
{
|
|
45
55
|
"path": "skills/codex/references/troubleshooting.md",
|
|
46
|
-
"sha256": "
|
|
56
|
+
"sha256": "834d2c6626dc90c009aebac3ee7ff8977a1f97dfb66cf5b955fd6414e6e2a897",
|
|
47
57
|
"agentTarget": "codex"
|
|
48
58
|
},
|
|
49
59
|
{
|
|
@@ -56,14 +66,19 @@
|
|
|
56
66
|
"sha256": "fb0be4c9c9bc9fd68f4e7ed8e9b7e4c8677c415130d441c28590ba0df6cb5aae",
|
|
57
67
|
"agentTarget": "codex"
|
|
58
68
|
},
|
|
69
|
+
{
|
|
70
|
+
"path": "skills/codex/references/chatgpt-compliance.md",
|
|
71
|
+
"sha256": "b0d70446be90a3e76396207ff32e7dc9561f25f43fc313d80cc9d9afc12e9b99",
|
|
72
|
+
"agentTarget": "codex"
|
|
73
|
+
},
|
|
59
74
|
{
|
|
60
75
|
"path": "skills/codex/references/examples.md",
|
|
61
|
-
"sha256": "
|
|
76
|
+
"sha256": "776fc09103f1a0eacb8f0efc9fdc527bc478f5897d226dd340e6676e67b27632",
|
|
62
77
|
"agentTarget": "codex"
|
|
63
78
|
},
|
|
64
79
|
{
|
|
65
80
|
"path": "skills/claude-code/SKILL.md",
|
|
66
|
-
"sha256": "
|
|
81
|
+
"sha256": "859218a6d7b97bc9a2b7da698550c5505b2ffab8370aecd3a21814ab3f4dd05f",
|
|
67
82
|
"agentTarget": "claude-code"
|
|
68
83
|
},
|
|
69
84
|
{
|
|
@@ -88,7 +103,17 @@
|
|
|
88
103
|
},
|
|
89
104
|
{
|
|
90
105
|
"path": "skills/claude-code/references/authoring-workflow.md",
|
|
91
|
-
"sha256": "
|
|
106
|
+
"sha256": "eca8d1f2b98ee64f6beda4d983f0f0df9023ceed8bd00ffb9dd6b5025dcaa4e4",
|
|
107
|
+
"agentTarget": "claude-code"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"path": "skills/claude-code/references/connect-an-api.md",
|
|
111
|
+
"sha256": "5c7d763ad57e6d6b67e3e115da48acd5bef3687acd050b776416906076cec440",
|
|
112
|
+
"agentTarget": "claude-code"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"path": "skills/claude-code/references/experience-design.md",
|
|
116
|
+
"sha256": "f07ed621eb1e8cdfa08abd10d5a6331c7e9d8198da422b8b4ad54d23475b2d4d",
|
|
92
117
|
"agentTarget": "claude-code"
|
|
93
118
|
},
|
|
94
119
|
{
|
|
@@ -103,7 +128,7 @@
|
|
|
103
128
|
},
|
|
104
129
|
{
|
|
105
130
|
"path": "skills/claude-code/references/troubleshooting.md",
|
|
106
|
-
"sha256": "
|
|
131
|
+
"sha256": "834d2c6626dc90c009aebac3ee7ff8977a1f97dfb66cf5b955fd6414e6e2a897",
|
|
107
132
|
"agentTarget": "claude-code"
|
|
108
133
|
},
|
|
109
134
|
{
|
|
@@ -116,9 +141,14 @@
|
|
|
116
141
|
"sha256": "fb0be4c9c9bc9fd68f4e7ed8e9b7e4c8677c415130d441c28590ba0df6cb5aae",
|
|
117
142
|
"agentTarget": "claude-code"
|
|
118
143
|
},
|
|
144
|
+
{
|
|
145
|
+
"path": "skills/claude-code/references/chatgpt-compliance.md",
|
|
146
|
+
"sha256": "b0d70446be90a3e76396207ff32e7dc9561f25f43fc313d80cc9d9afc12e9b99",
|
|
147
|
+
"agentTarget": "claude-code"
|
|
148
|
+
},
|
|
119
149
|
{
|
|
120
150
|
"path": "skills/claude-code/references/examples.md",
|
|
121
|
-
"sha256": "
|
|
151
|
+
"sha256": "776fc09103f1a0eacb8f0efc9fdc527bc478f5897d226dd340e6676e67b27632",
|
|
122
152
|
"agentTarget": "claude-code"
|
|
123
153
|
}
|
|
124
154
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noodleseed/agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Self-checking, self-updating agent skills for the Noodle Seed CLI. Authored in this repo by @noodle-borg/agent-kit; this is the published, independently-versioned canonical skills artifact the CLI fetches and verifies.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: noodle-seed
|
|
3
3
|
description: Use when building, validating, testing, deploying, or operating a local or hosted Noodle Seed MCP server or app authored in TypeScript with the noodle CLI.
|
|
4
|
-
version: 0.
|
|
5
|
-
hash:
|
|
4
|
+
version: 0.14.0
|
|
5
|
+
hash: dc338e17bb0de722
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Noodle Seed
|
|
@@ -15,6 +15,8 @@ Use this skill for project-local Noodle Seed authoring in Claude Code; preserve
|
|
|
15
15
|
|
|
16
16
|
This CLI is agent-native: the cold-agent-path commands speak the `--json` envelope (hosted admin/ops commands are still being normalized). Drive the loop by parsing machine state, not human prose. The full envelope, exit codes, and output modes are in `references/agent-contract.md`.
|
|
17
17
|
|
|
18
|
+
Before authoring, design the experience — the funnel/handoff boundary, tools, widgets, display modes, and grounding — see `references/experience-design.md`. Then run the loop:
|
|
19
|
+
|
|
18
20
|
1. **Discover** — `noodle commands --json`: every command, subcommand, flag, and exit code (don't read source).
|
|
19
21
|
2. **Author** — edit `src/server.ts` (the configured entrypoint); follow the capability recipe in `references/sdk-surface.md` and `references/examples.md`.
|
|
20
22
|
3. **Validate** — `noodle validate --json`; on failure `{ok:false,error:{code,message,fix,next,errors:[{code,path,message}]}}` — the per-field detail is in `error.errors[]`.
|
|
@@ -35,11 +37,14 @@ Load these on demand:
|
|
|
35
37
|
- `references/cli-commands.md` — every `noodle` command, grouped by area.
|
|
36
38
|
- `references/compile-errors.md` — fix `noodle validate` errors by code.
|
|
37
39
|
- `references/authoring-workflow.md` — input paths (scrape / OpenAPI import / user interview), the fit check, the validate→test→dev repair loop, connectors, and secrets/variables.
|
|
40
|
+
- `references/connect-an-api.md` — given an API key: secure it, probe the live API to learn the real shape, model the connector, and prove real output before building.
|
|
41
|
+
- `references/experience-design.md` — design the app experience before authoring: funnel/handoff boundary, grounding, two-users, display modes, and the wireframe/UX spec.
|
|
38
42
|
- `references/widgets-and-apps.md` — MCP Apps, React `view` widgets, the widget hook surface, output shaping, and CSP.
|
|
39
43
|
- `references/test-in-hosts.md` — connect and test in ChatGPT (developer mode), Claude, agent hosts, and MCP Inspector.
|
|
40
44
|
- `references/troubleshooting.md` — runtime symptom → cause → fix, in-host and hosted.
|
|
41
45
|
- `references/deploy-and-ops.md` — login/link/deploy/status/access and hosted operations.
|
|
42
46
|
- `references/publishing.md` — submit to the ChatGPT apps directory and Claude connectors directory.
|
|
47
|
+
- `references/chatgpt-compliance.md` — the OpenAI Apps SDK pre-submission checklist (beyond the metadata gate): conversational value, helpful-UI-only, in-chat completion, UI guidelines, domain guardrails, privacy.
|
|
43
48
|
- `references/examples.md` — flagship example index and a canonical `server.ts`.
|
|
44
49
|
|
|
45
50
|
## Safety
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
- Fit check
|
|
7
7
|
- Repair loop
|
|
8
8
|
- Connectors
|
|
9
|
-
- HTTP connector example
|
|
10
|
-
-
|
|
9
|
+
- HTTP connector example (full server)
|
|
10
|
+
- Design tools for the model
|
|
11
11
|
- Compute connector example
|
|
12
12
|
- Tests
|
|
13
13
|
- Secrets and variables
|
|
@@ -38,81 +38,126 @@ Tools record connector calls into a flow; recording is not execution. Do not bra
|
|
|
38
38
|
|
|
39
39
|
HTTP connector auth variants: `bearer` (`{ kind: "bearer", secret: secret("API_TOKEN") }`), `apiKey` (`{ kind: "apiKey", header: "X-API-Key", secret: secret("API_KEY") }`), `clientCredentials`, `delegatedOAuth`, and `delegatedSessionCookie`. Use managed `secret(...)` / `variable(...)` refs for all values that differ by org/app/env.
|
|
40
40
|
|
|
41
|
-
## HTTP connector example
|
|
41
|
+
## HTTP connector example (full server)
|
|
42
42
|
|
|
43
|
-
The operation mapping in detail: `request`
|
|
43
|
+
Declare the API as data, bind it with `use`, then record calls in tools. The operation mapping in detail: `request` builds the JSON request body, `query: [...]` names the input args sent as URL query parameters, and `response` maps the parsed HTTP body (bound to `${response}`) into your typed `output`. `auth` reads a managed `secret(...)` — never inline a key. This whole example is compile-verified on every `pnpm test`.
|
|
44
44
|
|
|
45
45
|
```ts
|
|
46
|
-
import { connector, secret, variable } from '@noodleseed/one';
|
|
46
|
+
import { connector, secret, variable, server, tool, z } from '@noodleseed/one';
|
|
47
47
|
|
|
48
48
|
const crm = connector('crm').version('1.0.0').http({
|
|
49
49
|
baseUrl: variable('CRM_BASE_URL'),
|
|
50
|
-
allowedOrigins: [
|
|
50
|
+
allowedOrigins: ['https://api.crm.example'],
|
|
51
51
|
auth: { kind: 'bearer', secret: secret('CRM_TOKEN') },
|
|
52
52
|
operations: {
|
|
53
|
-
|
|
53
|
+
find_customer: {
|
|
54
54
|
type: 'read',
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
response: { id:
|
|
55
|
+
method: 'GET',
|
|
56
|
+
path: '/customers',
|
|
57
|
+
query: ['email'],
|
|
58
|
+
input: { email: { type: 'string', required: true } },
|
|
59
|
+
output: { id: { type: 'string', required: true }, name: { type: 'string' } },
|
|
60
|
+
response: { id: '${response.data[0].id}', name: '${response.data[0].name}' },
|
|
61
61
|
},
|
|
62
|
-
|
|
62
|
+
create_ticket: {
|
|
63
63
|
type: 'action',
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
request: {
|
|
69
|
-
response: {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
path: '/tickets',
|
|
66
|
+
input: { customer_id: { type: 'string', required: true }, body: { type: 'string', required: true } },
|
|
67
|
+
output: { ticket_id: { type: 'string', required: true } },
|
|
68
|
+
request: { customer_id: '${args.customer_id}', body: '${args.body}' },
|
|
69
|
+
response: { ticket_id: '${response.id}' },
|
|
70
70
|
},
|
|
71
71
|
},
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
export default server('support', { title: 'Support', version: '1.0.0', use: { crm } }, [
|
|
75
|
+
tool('find_customer', {
|
|
76
|
+
description: 'Find a customer by email address.',
|
|
77
|
+
input: z.object({ email: z.string() }),
|
|
78
|
+
output: z.object({ id: z.string(), name: z.string().optional() }),
|
|
79
|
+
fulfil: ({ input, connectors }) => {
|
|
80
|
+
const customer = connectors.crm.find_customer({ email: input.email });
|
|
81
|
+
return { id: customer.id, name: customer.name };
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
tool('open_ticket', {
|
|
85
|
+
description: 'Open a support ticket for a customer.',
|
|
86
|
+
input: z.object({ customer_id: z.string(), body: z.string() }),
|
|
87
|
+
output: z.object({ ticket_id: z.string() }),
|
|
88
|
+
fulfil: ({ input, connectors }) => {
|
|
89
|
+
const ticket = connectors.crm.create_ticket({ customer_id: input.customer_id, body: input.body });
|
|
90
|
+
return { ticket_id: ticket.ticket_id };
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
]);
|
|
73
94
|
```
|
|
74
95
|
|
|
75
|
-
|
|
96
|
+
Naming: connector operation names and tool names are lowercase-with-underscores. Map with `${args.field}` for tool/operation inputs and `${response.path}` for the response — the parsed JSON body is bound directly to `${response}`, so there is **no `.body` envelope**; use bracket syntax for array indices (`${response.data[0].id}`) — a dotted numeric index like `.0.` is invalid. Declare URL query parameters with the operation-level `query: ["arg"]` array, **not** inside `request` (which builds only the JSON body). `allowedOrigins` must be literal origin URLs (the SSRF allowlist); `baseUrl` may be a `variable(...)` that differs by env.
|
|
97
|
+
|
|
98
|
+
More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `clientCredentials` | `delegatedOAuth` | `delegatedSessionCookie`. For client credentials use `{ kind: "clientCredentials", tokenUrl, clientId, clientSecret, scopes? }` (RFC-6749 grant); for a non-standard partner token endpoint add `profile: "custom"` with a `custom: { requestFormat, clientIdField, clientSecretField, tokenResponsePath, expirySource }` descriptor. Do not put credential headers in operation `headers`; use connector `auth`. For per-customer downstream calls use `delegatedOAuth` or `delegatedSessionCookie` with a matching `customerAuth` server option. Use `.compute(name, { input, output, run })` for a sandboxed transform; `provides:` (instead of `use:`) exposes a connector only to compute `callOperation`; and `noodle import openapi <file>` generates a connector from an OpenAPI spec.
|
|
99
|
+
|
|
100
|
+
## Design tools for the model
|
|
101
|
+
|
|
102
|
+
Design tools around what a user says, not 1:1 around API endpoints. A raw wrapper per endpoint (`get_task`, `list_tasks`, `close_task`) forces the model to orchestrate low-level calls and to know identifiers the user never sees — an MCP connector, but not a usable product. Instead:
|
|
76
103
|
|
|
77
|
-
|
|
104
|
+
- **Shape by intent.** Name and scope tools for the job to be done — "find my overdue tasks", "complete the task matching this text" — combining multiple backing calls in one recorded flow (`when(...)`) where it helps.
|
|
105
|
+
- **Prefer names/text over raw IDs.** When an action needs an id the user does not know, pair the id-taking operation with a find/search operation that returns model-friendly summaries (id + a human label), so the model resolves text → id itself. Write descriptions that tell the model when to use each tool and how they chain.
|
|
106
|
+
- **Return only what the model needs.** Map the response to a small, typed `output` (a few labelled fields), not the raw API payload.
|
|
78
107
|
|
|
79
|
-
|
|
108
|
+
This example pairs a name resolver with an id-taking action: the model calls `find_tasks` to turn the user’s words into an id, then `complete_task`. It is compile-verified on every `pnpm test`.
|
|
80
109
|
|
|
81
110
|
```ts
|
|
82
111
|
import { connector, secret, server, tool, z } from '@noodleseed/one';
|
|
83
112
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
response: { subject: '${response.data.subject}', status: '${response.data.state}' },
|
|
98
|
-
},
|
|
113
|
+
const tasks = connector('tasks').version('1.0.0').http({
|
|
114
|
+
baseUrl: 'https://api.tasks.example',
|
|
115
|
+
allowedOrigins: ['https://api.tasks.example'],
|
|
116
|
+
auth: { kind: 'bearer', secret: secret('TASKS_TOKEN') },
|
|
117
|
+
operations: {
|
|
118
|
+
search_tasks: {
|
|
119
|
+
type: 'read',
|
|
120
|
+
method: 'GET',
|
|
121
|
+
path: '/tasks',
|
|
122
|
+
query: ['query'],
|
|
123
|
+
input: { query: { type: 'string', required: true } },
|
|
124
|
+
output: { matches: { type: 'array', required: true } },
|
|
125
|
+
response: { matches: '${response.results}' },
|
|
99
126
|
},
|
|
100
|
-
|
|
127
|
+
close_task: {
|
|
128
|
+
type: 'action',
|
|
129
|
+
method: 'POST',
|
|
130
|
+
path: '/tasks/{id}/close',
|
|
131
|
+
input: { id: { type: 'string', required: true } },
|
|
132
|
+
output: { ok: { type: 'boolean', required: true } },
|
|
133
|
+
response: { ok: '${response.ok}' },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
});
|
|
101
137
|
|
|
102
|
-
export default server('
|
|
103
|
-
tool('
|
|
104
|
-
description: '
|
|
105
|
-
input: z.object({
|
|
106
|
-
output: z.object({
|
|
138
|
+
export default server('todo', { title: 'Tasks', version: '1.0.0', use: { tasks } }, [
|
|
139
|
+
tool('find_tasks', {
|
|
140
|
+
description: 'Find tasks whose text matches a query — call this first to resolve a task the user names by text into its id, then pass that id to complete_task.',
|
|
141
|
+
input: z.object({ query: z.string() }),
|
|
142
|
+
output: z.object({ matches: z.array(z.object({ id: z.string(), title: z.string() })) }),
|
|
143
|
+
fulfil: ({ input, connectors }) => {
|
|
144
|
+
const found = connectors.tasks.search_tasks({ query: input.query });
|
|
145
|
+
return { matches: found.matches };
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
tool('complete_task', {
|
|
149
|
+
description: 'Mark a task complete by its id (get the id from find_tasks).',
|
|
150
|
+
input: z.object({ id: z.string() }),
|
|
151
|
+
output: z.object({ ok: z.boolean() }),
|
|
107
152
|
fulfil: ({ input, connectors }) => {
|
|
108
|
-
const
|
|
109
|
-
return {
|
|
153
|
+
const result = connectors.tasks.close_task({ id: input.id });
|
|
154
|
+
return { ok: result.ok };
|
|
110
155
|
},
|
|
111
156
|
}),
|
|
112
157
|
]);
|
|
113
158
|
```
|
|
114
159
|
|
|
115
|
-
|
|
160
|
+
The model never sees a task id from the user; `find_tasks` returns `{ id, title }` summaries it can pick from, then `complete_task` acts by id. Keep write actions (`complete_task`) separate and explicitly described so the host can gate them.
|
|
116
161
|
|
|
117
162
|
## Compute connector example
|
|
118
163
|
|
|
@@ -120,10 +165,10 @@ More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `clientCredentials`
|
|
|
120
165
|
const scoring = connector('scoring').version('1.0.0').compute('normalize', {
|
|
121
166
|
input: { email: { type: "string", required: true }, priority: { type: "string" } },
|
|
122
167
|
output: { score: { type: "number", required: true } },
|
|
123
|
-
calls: {
|
|
168
|
+
calls: { find_customer: 'crm.find_customer' },
|
|
124
169
|
limits: { timeoutMs: 1000, maxHostCalls: 2 },
|
|
125
170
|
run(input, { callOperation }) {
|
|
126
|
-
const customer = callOperation("
|
|
171
|
+
const customer = callOperation("find_customer", { email: input.email }) as { id?: string };
|
|
127
172
|
return { score: customer.id && input.priority === "high" ? 100 : 50 };
|
|
128
173
|
},
|
|
129
174
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# ChatGPT App compliance (pre-submission)
|
|
2
|
+
|
|
3
|
+
`noodle check --target chatgpt` verifies the *metadata* prerequisites; app-store submission also faces a
|
|
4
|
+
human review against OpenAI’s Apps SDK UX principles. Run this checklist against the built app before
|
|
5
|
+
submitting, and render it as an audit table in the design wireframe (`design/wireframe.html` in the
|
|
6
|
+
`acme-*` examples) so partners and reviewers see it up front.
|
|
7
|
+
|
|
8
|
+
## Contents
|
|
9
|
+
|
|
10
|
+
- Metadata gate vs review
|
|
11
|
+
- Pre-submission checklist
|
|
12
|
+
- UI guidelines
|
|
13
|
+
- Domain guardrails
|
|
14
|
+
- Privacy and data
|
|
15
|
+
|
|
16
|
+
## Metadata gate vs review
|
|
17
|
+
|
|
18
|
+
`noodle check --target chatgpt --json` returning `ok:true` means the widget is *metadata-ready* (widget
|
|
19
|
+
`domain`, `openai/outputTemplate`, CSP, tool annotations, and `invoking`/`invoked` invocation copy are
|
|
20
|
+
present) — it does NOT prove host rendering, conversation UX, or submission acceptance. Validate real
|
|
21
|
+
rendering in ChatGPT Developer Mode / MCP Inspector, then run the checklist below.
|
|
22
|
+
|
|
23
|
+
## Pre-submission checklist (what review looks for)
|
|
24
|
+
|
|
25
|
+
1. **Conversational value** — at least one capability relies on ChatGPT’s strengths: natural-language
|
|
26
|
+
actions no tap-driven app can do (e.g. "two margheritas and a lemon tart" parses into a cart). Cite
|
|
27
|
+
concrete app behavior, not aspirations.
|
|
28
|
+
2. **Beyond base ChatGPT** — new knowledge, actions, or presentation (grounded partner data, live
|
|
29
|
+
inventory, signed handoffs, real-world routing).
|
|
30
|
+
3. **Atomic, model-friendly actions** — self-contained tools with explicit input/output schemas, and an
|
|
31
|
+
annotation on every tool (`annotations.readOnly()` / `.action()` / `.openAction()`).
|
|
32
|
+
4. **Helpful UI only** — justify each widget (would plain text degrade UX?), and note what you
|
|
33
|
+
deliberately did NOT build a widget for (payment is off-app → no payment widget).
|
|
34
|
+
5. **In-chat task completion** — the user finishes a meaningful task in chat. For a top-of-funnel app,
|
|
35
|
+
the task is the discovery/config loop completed in-chat with an intentional handoff.
|
|
36
|
+
6. **Performance** — tool calls scoped per step; response-time targets stated.
|
|
37
|
+
7. **Discoverability** — broad, natural trigger prompts listed; description keywords planned. Golden
|
|
38
|
+
prompt sets and metadata optimization are a launch workstream, not polish.
|
|
39
|
+
8. **Platform fit** — multi-turn dialogue, conversation memory, and multimodality where genuinely useful.
|
|
40
|
+
|
|
41
|
+
## UI guidelines
|
|
42
|
+
|
|
43
|
+
System fonts, monochrome outlined icons, WCAG AA contrast, at most two actions on inline cards, no nested
|
|
44
|
+
scroll, and the right display mode per intent (inline by default; fullscreen only where browsing needs
|
|
45
|
+
it; picture-in-picture only for live state). Brand only through `server` `branding` tokens — accent on
|
|
46
|
+
the primary CTA, logo, and badges, nothing else; the compiler derives the palette. Never inject raw
|
|
47
|
+
global CSS.
|
|
48
|
+
|
|
49
|
+
## Domain guardrails
|
|
50
|
+
|
|
51
|
+
For regulated-adjacent apps, add app-specific trust behaviors and **show them in the rendered pixels**:
|
|
52
|
+
cite the source and its revision for consequential lookups; frame regulated content as "considerations,
|
|
53
|
+
not a ruling"; never invent compatibility, availability, or pricing; and always show the relevant
|
|
54
|
+
caution/disclaimer. These are what make a regulated-adjacent app approvable.
|
|
55
|
+
|
|
56
|
+
## Privacy and data
|
|
57
|
+
|
|
58
|
+
Data flows through OpenAI; tool payloads and whatever the server stores must match the partner’s privacy
|
|
59
|
+
policy. No payment happens in chat (PCI stays off-app). Avoid per-user OAuth in a top-of-funnel v1 (use
|
|
60
|
+
service credentials via a `connector`); add end-user auth only for two-way apps (`customerAuth`). Keep
|
|
61
|
+
secrets out of tool output, widgets, and logs. If the partner’s published policy predates the app, flag a
|
|
62
|
+
privacy gap for their counsel before submission. Re-run this checklist against the *built* app before
|
|
63
|
+
every submission — not just the wireframe.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Connect a live API (you were given a key)
|
|
2
|
+
|
|
3
|
+
When the user hands you an API key or credentials, don't infer the data from documentation — docs
|
|
4
|
+
drift. Probe the live API, learn the real shape, then encode it as a `connector`. The loop:
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- Secure the key first
|
|
9
|
+
- Probe the live API
|
|
10
|
+
- Model the connector from the observed shape
|
|
11
|
+
- Design intent tools
|
|
12
|
+
- Prove real output
|
|
13
|
+
- Then build the app
|
|
14
|
+
|
|
15
|
+
## Secure the key first
|
|
16
|
+
|
|
17
|
+
Never inline or log the key. Have the user put it in an environment variable, then store it as a
|
|
18
|
+
managed secret and reference it only as `secret(...)`:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
export SOME_API_KEY=… # the user sets this; it never appears in a file or prompt
|
|
22
|
+
noodle secrets set SOME_API_KEY --from-env SOME_API_KEY
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
In `server.ts` the key is only ever `secret("SOME_API_KEY")` — keep the raw value out of code, tests,
|
|
26
|
+
prompts, logs, and generated files.
|
|
27
|
+
|
|
28
|
+
## Probe the live API
|
|
29
|
+
|
|
30
|
+
Learn the actual response shape empirically. Two ways — capture one real example response per endpoint
|
|
31
|
+
you will use, and read its field names, nesting, array shapes, pagination, and id-vs-label fields:
|
|
32
|
+
|
|
33
|
+
- **With your own HTTP/shell tool** — call a representative read endpoint using the key **from the env
|
|
34
|
+
var**, never the literal (so it stays out of logs): `curl -H "Authorization: Bearer $SOME_API_KEY"
|
|
35
|
+
https://api.example.com/things`. Inspect the returned JSON.
|
|
36
|
+
- **Noodle-native** — author a minimal read operation that maps the whole body (`response: { raw:
|
|
37
|
+
'${response}' }`), `noodle secrets set` the key, then `noodle tools call` it to see the real payload
|
|
38
|
+
in-process.
|
|
39
|
+
|
|
40
|
+
## Model the connector from the observed shape
|
|
41
|
+
|
|
42
|
+
Encode the API as an HTTP connector, mapping only the fields you actually saw into a small typed
|
|
43
|
+
`output`:
|
|
44
|
+
|
|
45
|
+
- `connector("id").version("1.0.0").http({ baseUrl, allowedOrigins, auth, operations })`.
|
|
46
|
+
- `auth: { kind: 'bearer', secret: secret('SOME_API_KEY') }` — or `{ kind: 'apiKey', header: 'X-API-Key',
|
|
47
|
+
secret: secret('SOME_API_KEY') }`. Never put the credential in operation `headers`.
|
|
48
|
+
- Per operation: `method`, `path` (with `{id}` templates), `query: ["arg"]` for URL params, `input`,
|
|
49
|
+
`output`, and a `response` mapping whose `${response.path}` matches the real JSON — the parsed body is
|
|
50
|
+
bound directly to `${response}` (no `.body` envelope); use bracket indices for arrays
|
|
51
|
+
(`${response.results[0].id}`).
|
|
52
|
+
|
|
53
|
+
The full connector shape, every `auth.kind`, and compute connectors are in
|
|
54
|
+
`references/authoring-workflow.md`.
|
|
55
|
+
|
|
56
|
+
## Design intent tools
|
|
57
|
+
|
|
58
|
+
Shape tools around what the user says, not 1:1 around endpoints. Pair an id-taking action with a
|
|
59
|
+
find/search operation that returns `{ id, label }` summaries so the model resolves text → id itself,
|
|
60
|
+
and map each response to a few labelled fields the model can speak from. See the "Design tools for the
|
|
61
|
+
model" section of `references/authoring-workflow.md`.
|
|
62
|
+
|
|
63
|
+
## Prove real output
|
|
64
|
+
|
|
65
|
+
`noodle validate` / `noodle test` prove a connector tool *compiles and registers* — not that its
|
|
66
|
+
mapping returns data. With the secret set, run a live read: `noodle tools call <read_tool> --args
|
|
67
|
+
'{…}'` executes the connector against the real API in-process. Confirm the mapped fields are populated,
|
|
68
|
+
not `undefined`; if they are empty, fix the `${response…}` paths against the real payload and re-run.
|
|
69
|
+
Only run a live write if it is safe or the user approved it.
|
|
70
|
+
|
|
71
|
+
## Then build the app
|
|
72
|
+
|
|
73
|
+
With real data flowing, design the experience (`references/experience-design.md`), add widgets where a
|
|
74
|
+
UI genuinely helps (`references/widgets-and-apps.md`), and verify with `noodle check`. Deploy per
|
|
75
|
+
`references/deploy-and-ops.md`, and set the same secret in the hosted environment with `noodle secrets
|
|
76
|
+
set` before the first hosted call.
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# Examples
|
|
2
2
|
|
|
3
|
-
Flagship examples (one per capability). Read the matching example for the pattern you need; extend an existing flagship rather than inventing a new shape.
|
|
3
|
+
Flagship examples (one per capability). Each lives in the Noodle Seed repository under `examples/<name>/` (`server.ts`, `src/views/*.tsx`, `README.md`, and a `design/` folder for the design-first flagships). Read the matching example for the pattern you need; extend an existing flagship rather than inventing a new shape.
|
|
4
4
|
|
|
5
5
|
| Example | Use when |
|
|
6
6
|
| :-- | :-- |
|
|
7
7
|
| `hello` | Minimal TypeScript quickstart — a single tool, no connectors/widgets. |
|
|
8
8
|
| `weather` | HTTP connectors, multi-step flows, and the sandboxed compute connector. |
|
|
9
9
|
| `food-ordering` | Consumer ordering MCP App widgets, app-only helpers, cart state, assets, branding, and handoff. |
|
|
10
|
+
| `acme-discovery` | Top-of-funnel discovery→handoff: a discovery carousel, a `create_handoff` deep link, and a design-first UX spec + wireframe. |
|
|
11
|
+
| `acme-tasks` | A two-way productivity app designed around its top-3 prioritized flows (capture/prioritize/complete), with a design-first flow spec + wireframe. |
|
|
12
|
+
| `acme-bistro` | End-to-end ordering with a payment-only handoff; ships a gold-standard `design/` set (UX doc, wireframe with compliance audit, API contract). |
|
|
10
13
|
| `customer-auth` | End-user (customer) auth via OIDC/Firebase bridge with delegated credentials. |
|
|
11
14
|
| `stateful-draft` | Durable, caller-scoped widget state handles with optimistic revisions. |
|
|
12
15
|
| `perplexity` | A real SaaS API with bearer auth and a managed `secret`. |
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Experience design
|
|
2
|
+
|
|
3
|
+
Design the app experience before you author it. A ChatGPT app is a conversation with tools and
|
|
4
|
+
widgets, not a website — decide the funnel, the tools, the widgets, and the handoff first, then
|
|
5
|
+
build with the SDK (`references/authoring-workflow.md`, `references/widgets-and-apps.md`). This
|
|
6
|
+
reference is the design discipline; the build references are the mechanics.
|
|
7
|
+
|
|
8
|
+
## Contents
|
|
9
|
+
|
|
10
|
+
- Design first
|
|
11
|
+
- The handoff is the product
|
|
12
|
+
- Grounded, never guessing
|
|
13
|
+
- Two users: the human and the model
|
|
14
|
+
- ChatGPT-native surface
|
|
15
|
+
- Scope discipline and auth stance
|
|
16
|
+
- Wireframe and UX-spec anatomy
|
|
17
|
+
- The deliverables
|
|
18
|
+
- From design to build
|
|
19
|
+
|
|
20
|
+
## Design first
|
|
21
|
+
|
|
22
|
+
Write a short design spec before you author `server.ts` — a `SPEC.md`-style note you keep updated as
|
|
23
|
+
the app evolves. It has required fields:
|
|
24
|
+
|
|
25
|
+
- **Funnel boundary** — one sentence stating exactly what happens in ChatGPT and what happens off-app.
|
|
26
|
+
- **Tools** — each model-visible tool, snake_case, with what it takes and returns.
|
|
27
|
+
- **Widgets and display modes** — each widget, PascalCase, and the display mode it renders in
|
|
28
|
+
(inline card / carousel / fullscreen / picture-in-picture). State the modes you deliberately do
|
|
29
|
+
*not* use, and why.
|
|
30
|
+
- **Grounding sources** — where each fact comes from (a `connector` operation, a provided dataset).
|
|
31
|
+
- **Handoff domains** — the external origins the app links out to (these become server
|
|
32
|
+
`handoff.allowedDomains`).
|
|
33
|
+
- **Exceptions** — any deliberate departure from the defaults below, each with its reason.
|
|
34
|
+
|
|
35
|
+
Then author, preview with `noodle dev` / `noodle devtools`, and iterate. The spec is the contract the
|
|
36
|
+
tools, widgets, and handoff all trace back to.
|
|
37
|
+
|
|
38
|
+
## The handoff is the product
|
|
39
|
+
|
|
40
|
+
Default scope is **top-of-funnel**: ChatGPT owns discovery, intent-shaping, and configuration; the
|
|
41
|
+
partner's own platform owns the transaction, the account, and everything after. The app hands off with
|
|
42
|
+
a deep link that carries the configured state (a pre-filled cart, a chosen configuration), and the
|
|
43
|
+
external target is declared in the server-level `handoff.allowedDomains`. This is a feature: it keeps
|
|
44
|
+
payment and account burden off the app and keeps the partner's platform central.
|
|
45
|
+
|
|
46
|
+
Deliberate exceptions exist — an app that completes the transaction in-chat (handing off for payment
|
|
47
|
+
only), or a two-way app that reads and writes a connected account. When you break the top-of-funnel
|
|
48
|
+
default, write down why in the design spec.
|
|
49
|
+
|
|
50
|
+
## Grounded, never guessing
|
|
51
|
+
|
|
52
|
+
The app's credibility is that it answers from the partner's own data, reached through a `connector`,
|
|
53
|
+
never invented. Never fabricate compatibility, availability, pricing, or eligibility. For
|
|
54
|
+
consequential lookups, **cite the source and its revision in the widget** (e.g. a spec-sheet name and
|
|
55
|
+
revision date) and route unknowns to a human path rather than improvising. Guardrails belong in the
|
|
56
|
+
rendered pixels, not just in prose.
|
|
57
|
+
|
|
58
|
+
## Two users: the human and the model
|
|
59
|
+
|
|
60
|
+
Every tool and widget serves two users at once — the human who interacts and the model that reads the
|
|
61
|
+
result. Keep tools atomic with inputs the model can fill from natural language, and return enough
|
|
62
|
+
structure that the model can speak confidently in one round-trip. Build a widget only where plain text
|
|
63
|
+
would genuinely degrade the experience (photos, comparisons, carts, configuration). If text would do,
|
|
64
|
+
don't build the widget.
|
|
65
|
+
|
|
66
|
+
## ChatGPT-native surface
|
|
67
|
+
|
|
68
|
+
Widgets should read as native to the host. Brand through the `server` `branding` tokens only — the
|
|
69
|
+
compiler derives the palette — and reserve the accent for the primary CTA, the logo, and badges. No
|
|
70
|
+
brand gradients, no app-shoved-into-chat. Keep inline cards to at most two actions and avoid nested
|
|
71
|
+
scroll. Pick each display mode deliberately (fullscreen only where browsing genuinely needs it;
|
|
72
|
+
picture-in-picture only for live ongoing state). See `references/widgets-and-apps.md` for the widget
|
|
73
|
+
mechanics — do not restyle with raw global CSS.
|
|
74
|
+
|
|
75
|
+
## Scope discipline and auth stance
|
|
76
|
+
|
|
77
|
+
Lock the funnel boundary in writing before designing, and keep a "future enhancements" list as the
|
|
78
|
+
pressure valve. No payment happens in chat. Avoid per-user auth in a top-of-funnel v1 — use the
|
|
79
|
+
partner's service credentials via a `connector`; add end-user auth only when the app is two-way by
|
|
80
|
+
nature (see the `customer-auth` example in `references/examples.md`).
|
|
81
|
+
|
|
82
|
+
## Wireframe and UX-spec anatomy
|
|
83
|
+
|
|
84
|
+
For anything non-trivial, sketch the experience as a wireframe before authoring — a walk through the
|
|
85
|
+
conversation, screen by screen. Each screen shows a real user message, the **tool call that precedes
|
|
86
|
+
the widget**, and the widget filled with plausible, internally consistent data (never lorem ipsum).
|
|
87
|
+
Label each widget with its component name so the wireframe, the spec, and the code share one
|
|
88
|
+
vocabulary; put the funnel boundary at the top; and render off-app destinations distinctly (they are
|
|
89
|
+
reached only after the handoff). Each wireframe screen maps directly to noodle: a screen with a widget
|
|
90
|
+
is a `toolWithWidget` + a React `view`; a plain answer is a `tool`; an off-app destination is a
|
|
91
|
+
`handoff.allowedDomains` entry. A compact single screen, anonymized to a fictional "Acme" business:
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
<div class="phone">
|
|
95
|
+
<div class="chatgpt-header">ChatGPT · Acme</div>
|
|
96
|
+
<div class="msg user">14kW array, ~40kWh battery — what pairs with the Acme X?</div>
|
|
97
|
+
<!-- tool call precedes every widget; the model fills inputs from the message -->
|
|
98
|
+
<div class="tool-call">check_compatibility { model: "Acme X", battery_kwh: 40 }</div>
|
|
99
|
+
<div class="wcard">
|
|
100
|
+
<div class="wcard-head">CompatibilityCard</div> <!-- component name = code + spec -->
|
|
101
|
+
<div class="wcard-body">
|
|
102
|
+
<div class="kv">Acme X ⇄ 40kWh pack <span class="badge certified">Compatible</span></div>
|
|
103
|
+
<div class="cite">Source: Acme Integration Guide, Rev 7 (2026)</div> <!-- grounded + cited -->
|
|
104
|
+
<a class="cta">Configure system</a> <!-- ≤2 actions on an inline card -->
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Expand from one screen to the full journey: entry → configuration → handoff, ending at the off-app
|
|
111
|
+
destination. Verify the built app against the design with `noodle check --target chatgpt`.
|
|
112
|
+
|
|
113
|
+
For the full quality bar, open a shipped gold-standard set: in the Noodle Seed repository, the
|
|
114
|
+
`design/` folder of the `acme-discovery` (top-of-funnel), `acme-tasks` (two-way), and `acme-bistro`
|
|
115
|
+
(end-to-end) examples each holds a house-style UX Document and a single-file HTML wireframe with an
|
|
116
|
+
embedded OpenAI Apps SDK compliance audit. Match that bar.
|
|
117
|
+
|
|
118
|
+
## The deliverables
|
|
119
|
+
|
|
120
|
+
The design phase produces up to three artifacts — worked gold-standard versions live in each
|
|
121
|
+
`acme-*` example's `design/` folder (copy their structure, swap the content):
|
|
122
|
+
|
|
123
|
+
- **UX Document** — the thinking artifact. House-style sections: product overview / knowledge base,
|
|
124
|
+
competitive landscape, personas, conversational flow (with tool-call playscripts), widget specs +
|
|
125
|
+
a display-mode strategy, tool definitions, conversation guardrails, journey map, handoff/auth
|
|
126
|
+
architecture, demo scope, success metrics, and future enhancements — opening on the funnel-boundary
|
|
127
|
+
line every scope debate resolves against.
|
|
128
|
+
- **Wireframe** — the single-file HTML alignment artifact (anatomy above) with the embedded compliance
|
|
129
|
+
audit; see `references/chatgpt-compliance.md`.
|
|
130
|
+
- **API contract** — when the partner's backend must be built or wrapped. Escalate: (1) the MCP
|
|
131
|
+
tool→call-sequence map (always); (2) "Recommended API Shapes" — concrete request/response JSON per
|
|
132
|
+
tool, including the hardest nested case; (3) a full OpenAPI spec for transactional apps. Contract
|
|
133
|
+
rules: shape responses for one-round-trip rendering (embed related objects, pre-sort); put stable
|
|
134
|
+
ids on anything the user picks; keep validation in the API, not the model; the handoff endpoint
|
|
135
|
+
mints a signed, expiring URL + attribution and never proxies payment; use server-side partner
|
|
136
|
+
credentials for v1 (per-user auth only for two-way apps); name tools for user intent.
|
|
137
|
+
|
|
138
|
+
## From design to build
|
|
139
|
+
|
|
140
|
+
Once the design spec is settled, build it: `references/authoring-workflow.md` for the author→validate
|
|
141
|
+
loop and connectors, `references/widgets-and-apps.md` for widgets and CSP, `references/examples.md`
|
|
142
|
+
for a flagship to extend, and `references/deploy-and-ops.md` to ship.
|
|
@@ -25,6 +25,6 @@ For protocol/conformance checks, the headless harness is `@mcpjam/cli`, not a `n
|
|
|
25
25
|
| `noodle validate` passes but React views fail to bundle (“requires Vite”) | Project dependencies are not installed — widget bundling uses the app-local Vite | Run the project’s package install, then retry `noodle dev` / `noodle deploy` |
|
|
26
26
|
| Hosted endpoint returns 401 to probes | Expected: hosted servers challenge unauthenticated calls with OAuth metadata | Sign in from the host when prompted; widen who may call with `noodle access set` if testers are outside the org |
|
|
27
27
|
| Tools error only after deploy | Runtime/config differences surface hosted (secrets, connector reachability) | Run `noodle smoke`, then `noodle metrics --agent-output` and `noodle events --tool <name> --status tool_error --json`; check `noodle secrets list` scope |
|
|
28
|
-
| A connector tool validates and lists, but returns empty or `undefined` fields | The `response` mapping references a path the API does not return — usually the wrong root (
|
|
28
|
+
| A connector tool validates and lists, but returns empty or `undefined` fields | The `response` mapping references a path the API does not return — usually the wrong root (a `.body` segment, when the parsed body is bound directly to `${response}`) or the wrong shape | Run `noodle tools call <name> --args <json>` with the secret set and compare the mapped result to the API’s real JSON; map from `${response.<path>}` (the body is `${response}`, there is no `.body`) and use bracket array indices (`${response.items[0].id}`) |
|
|
29
29
|
| Need to invoke a tool from the terminal | Local tools run in-process; the `noodle` CLI is not a general MCP client for **deployed** URLs (there is no `call <url>` verb) | Locally, `noodle tools call <name> --args <json>` (also `noodle resources read` / `noodle prompts get`) runs the tool against the in-process runtime — with the secret set it executes the connector against the real API, so use it to prove mapped output. For a **deployed** URL use MCP Inspector or `npx @mcpjam/cli@latest tools call --url <url> ...` |
|
|
30
30
|
| One customer/session reports a bad answer or protocol error | The failure may be a model/tool error, host protocol error, or connector/runtime error | Run `noodle metrics --agent-output`, then `noodle events --tool <name> --status tool_error --json`; copy the `sessionId` into `noodle events --session <id> --json`, then match timestamps with `noodle logs` |
|
package/skills/codex/SKILL.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: noodle-seed
|
|
3
3
|
description: Use when building, validating, testing, deploying, or operating a local or hosted Noodle Seed MCP server or app authored in TypeScript with the noodle CLI.
|
|
4
|
-
version: 0.
|
|
5
|
-
hash:
|
|
4
|
+
version: 0.14.0
|
|
5
|
+
hash: e623fd6ca7caad0f
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Noodle Seed
|
|
@@ -15,6 +15,8 @@ Use this skill for local Noodle Seed project work in Codex.
|
|
|
15
15
|
|
|
16
16
|
This CLI is agent-native: the cold-agent-path commands speak the `--json` envelope (hosted admin/ops commands are still being normalized). Drive the loop by parsing machine state, not human prose. The full envelope, exit codes, and output modes are in `references/agent-contract.md`.
|
|
17
17
|
|
|
18
|
+
Before authoring, design the experience — the funnel/handoff boundary, tools, widgets, display modes, and grounding — see `references/experience-design.md`. Then run the loop:
|
|
19
|
+
|
|
18
20
|
1. **Discover** — `noodle commands --json`: every command, subcommand, flag, and exit code (don't read source).
|
|
19
21
|
2. **Author** — edit `src/server.ts` (the configured entrypoint); follow the capability recipe in `references/sdk-surface.md` and `references/examples.md`.
|
|
20
22
|
3. **Validate** — `noodle validate --json`; on failure `{ok:false,error:{code,message,fix,next,errors:[{code,path,message}]}}` — the per-field detail is in `error.errors[]`.
|
|
@@ -35,11 +37,14 @@ Load these on demand:
|
|
|
35
37
|
- `references/cli-commands.md` — every `noodle` command, grouped by area.
|
|
36
38
|
- `references/compile-errors.md` — fix `noodle validate` errors by code.
|
|
37
39
|
- `references/authoring-workflow.md` — input paths (scrape / OpenAPI import / user interview), the fit check, the validate→test→dev repair loop, connectors, and secrets/variables.
|
|
40
|
+
- `references/connect-an-api.md` — given an API key: secure it, probe the live API to learn the real shape, model the connector, and prove real output before building.
|
|
41
|
+
- `references/experience-design.md` — design the app experience before authoring: funnel/handoff boundary, grounding, two-users, display modes, and the wireframe/UX spec.
|
|
38
42
|
- `references/widgets-and-apps.md` — MCP Apps, React `view` widgets, the widget hook surface, output shaping, and CSP.
|
|
39
43
|
- `references/test-in-hosts.md` — connect and test in ChatGPT (developer mode), Claude, agent hosts, and MCP Inspector.
|
|
40
44
|
- `references/troubleshooting.md` — runtime symptom → cause → fix, in-host and hosted.
|
|
41
45
|
- `references/deploy-and-ops.md` — login/link/deploy/status/access and hosted operations.
|
|
42
46
|
- `references/publishing.md` — submit to the ChatGPT apps directory and Claude connectors directory.
|
|
47
|
+
- `references/chatgpt-compliance.md` — the OpenAI Apps SDK pre-submission checklist (beyond the metadata gate): conversational value, helpful-UI-only, in-chat completion, UI guidelines, domain guardrails, privacy.
|
|
43
48
|
- `references/examples.md` — flagship example index and a canonical `server.ts`.
|
|
44
49
|
|
|
45
50
|
## Safety
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
- Fit check
|
|
7
7
|
- Repair loop
|
|
8
8
|
- Connectors
|
|
9
|
-
- HTTP connector example
|
|
10
|
-
-
|
|
9
|
+
- HTTP connector example (full server)
|
|
10
|
+
- Design tools for the model
|
|
11
11
|
- Compute connector example
|
|
12
12
|
- Tests
|
|
13
13
|
- Secrets and variables
|
|
@@ -38,81 +38,126 @@ Tools record connector calls into a flow; recording is not execution. Do not bra
|
|
|
38
38
|
|
|
39
39
|
HTTP connector auth variants: `bearer` (`{ kind: "bearer", secret: secret("API_TOKEN") }`), `apiKey` (`{ kind: "apiKey", header: "X-API-Key", secret: secret("API_KEY") }`), `clientCredentials`, `delegatedOAuth`, and `delegatedSessionCookie`. Use managed `secret(...)` / `variable(...)` refs for all values that differ by org/app/env.
|
|
40
40
|
|
|
41
|
-
## HTTP connector example
|
|
41
|
+
## HTTP connector example (full server)
|
|
42
42
|
|
|
43
|
-
The operation mapping in detail: `request`
|
|
43
|
+
Declare the API as data, bind it with `use`, then record calls in tools. The operation mapping in detail: `request` builds the JSON request body, `query: [...]` names the input args sent as URL query parameters, and `response` maps the parsed HTTP body (bound to `${response}`) into your typed `output`. `auth` reads a managed `secret(...)` — never inline a key. This whole example is compile-verified on every `pnpm test`.
|
|
44
44
|
|
|
45
45
|
```ts
|
|
46
|
-
import { connector, secret, variable } from '@noodleseed/one';
|
|
46
|
+
import { connector, secret, variable, server, tool, z } from '@noodleseed/one';
|
|
47
47
|
|
|
48
48
|
const crm = connector('crm').version('1.0.0').http({
|
|
49
49
|
baseUrl: variable('CRM_BASE_URL'),
|
|
50
|
-
allowedOrigins: [
|
|
50
|
+
allowedOrigins: ['https://api.crm.example'],
|
|
51
51
|
auth: { kind: 'bearer', secret: secret('CRM_TOKEN') },
|
|
52
52
|
operations: {
|
|
53
|
-
|
|
53
|
+
find_customer: {
|
|
54
54
|
type: 'read',
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
response: { id:
|
|
55
|
+
method: 'GET',
|
|
56
|
+
path: '/customers',
|
|
57
|
+
query: ['email'],
|
|
58
|
+
input: { email: { type: 'string', required: true } },
|
|
59
|
+
output: { id: { type: 'string', required: true }, name: { type: 'string' } },
|
|
60
|
+
response: { id: '${response.data[0].id}', name: '${response.data[0].name}' },
|
|
61
61
|
},
|
|
62
|
-
|
|
62
|
+
create_ticket: {
|
|
63
63
|
type: 'action',
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
request: {
|
|
69
|
-
response: {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
path: '/tickets',
|
|
66
|
+
input: { customer_id: { type: 'string', required: true }, body: { type: 'string', required: true } },
|
|
67
|
+
output: { ticket_id: { type: 'string', required: true } },
|
|
68
|
+
request: { customer_id: '${args.customer_id}', body: '${args.body}' },
|
|
69
|
+
response: { ticket_id: '${response.id}' },
|
|
70
70
|
},
|
|
71
71
|
},
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
export default server('support', { title: 'Support', version: '1.0.0', use: { crm } }, [
|
|
75
|
+
tool('find_customer', {
|
|
76
|
+
description: 'Find a customer by email address.',
|
|
77
|
+
input: z.object({ email: z.string() }),
|
|
78
|
+
output: z.object({ id: z.string(), name: z.string().optional() }),
|
|
79
|
+
fulfil: ({ input, connectors }) => {
|
|
80
|
+
const customer = connectors.crm.find_customer({ email: input.email });
|
|
81
|
+
return { id: customer.id, name: customer.name };
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
tool('open_ticket', {
|
|
85
|
+
description: 'Open a support ticket for a customer.',
|
|
86
|
+
input: z.object({ customer_id: z.string(), body: z.string() }),
|
|
87
|
+
output: z.object({ ticket_id: z.string() }),
|
|
88
|
+
fulfil: ({ input, connectors }) => {
|
|
89
|
+
const ticket = connectors.crm.create_ticket({ customer_id: input.customer_id, body: input.body });
|
|
90
|
+
return { ticket_id: ticket.ticket_id };
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
]);
|
|
73
94
|
```
|
|
74
95
|
|
|
75
|
-
|
|
96
|
+
Naming: connector operation names and tool names are lowercase-with-underscores. Map with `${args.field}` for tool/operation inputs and `${response.path}` for the response — the parsed JSON body is bound directly to `${response}`, so there is **no `.body` envelope**; use bracket syntax for array indices (`${response.data[0].id}`) — a dotted numeric index like `.0.` is invalid. Declare URL query parameters with the operation-level `query: ["arg"]` array, **not** inside `request` (which builds only the JSON body). `allowedOrigins` must be literal origin URLs (the SSRF allowlist); `baseUrl` may be a `variable(...)` that differs by env.
|
|
97
|
+
|
|
98
|
+
More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `clientCredentials` | `delegatedOAuth` | `delegatedSessionCookie`. For client credentials use `{ kind: "clientCredentials", tokenUrl, clientId, clientSecret, scopes? }` (RFC-6749 grant); for a non-standard partner token endpoint add `profile: "custom"` with a `custom: { requestFormat, clientIdField, clientSecretField, tokenResponsePath, expirySource }` descriptor. Do not put credential headers in operation `headers`; use connector `auth`. For per-customer downstream calls use `delegatedOAuth` or `delegatedSessionCookie` with a matching `customerAuth` server option. Use `.compute(name, { input, output, run })` for a sandboxed transform; `provides:` (instead of `use:`) exposes a connector only to compute `callOperation`; and `noodle import openapi <file>` generates a connector from an OpenAPI spec.
|
|
99
|
+
|
|
100
|
+
## Design tools for the model
|
|
101
|
+
|
|
102
|
+
Design tools around what a user says, not 1:1 around API endpoints. A raw wrapper per endpoint (`get_task`, `list_tasks`, `close_task`) forces the model to orchestrate low-level calls and to know identifiers the user never sees — an MCP connector, but not a usable product. Instead:
|
|
76
103
|
|
|
77
|
-
|
|
104
|
+
- **Shape by intent.** Name and scope tools for the job to be done — "find my overdue tasks", "complete the task matching this text" — combining multiple backing calls in one recorded flow (`when(...)`) where it helps.
|
|
105
|
+
- **Prefer names/text over raw IDs.** When an action needs an id the user does not know, pair the id-taking operation with a find/search operation that returns model-friendly summaries (id + a human label), so the model resolves text → id itself. Write descriptions that tell the model when to use each tool and how they chain.
|
|
106
|
+
- **Return only what the model needs.** Map the response to a small, typed `output` (a few labelled fields), not the raw API payload.
|
|
78
107
|
|
|
79
|
-
|
|
108
|
+
This example pairs a name resolver with an id-taking action: the model calls `find_tasks` to turn the user’s words into an id, then `complete_task`. It is compile-verified on every `pnpm test`.
|
|
80
109
|
|
|
81
110
|
```ts
|
|
82
111
|
import { connector, secret, server, tool, z } from '@noodleseed/one';
|
|
83
112
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
response: { subject: '${response.data.subject}', status: '${response.data.state}' },
|
|
98
|
-
},
|
|
113
|
+
const tasks = connector('tasks').version('1.0.0').http({
|
|
114
|
+
baseUrl: 'https://api.tasks.example',
|
|
115
|
+
allowedOrigins: ['https://api.tasks.example'],
|
|
116
|
+
auth: { kind: 'bearer', secret: secret('TASKS_TOKEN') },
|
|
117
|
+
operations: {
|
|
118
|
+
search_tasks: {
|
|
119
|
+
type: 'read',
|
|
120
|
+
method: 'GET',
|
|
121
|
+
path: '/tasks',
|
|
122
|
+
query: ['query'],
|
|
123
|
+
input: { query: { type: 'string', required: true } },
|
|
124
|
+
output: { matches: { type: 'array', required: true } },
|
|
125
|
+
response: { matches: '${response.results}' },
|
|
99
126
|
},
|
|
100
|
-
|
|
127
|
+
close_task: {
|
|
128
|
+
type: 'action',
|
|
129
|
+
method: 'POST',
|
|
130
|
+
path: '/tasks/{id}/close',
|
|
131
|
+
input: { id: { type: 'string', required: true } },
|
|
132
|
+
output: { ok: { type: 'boolean', required: true } },
|
|
133
|
+
response: { ok: '${response.ok}' },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
});
|
|
101
137
|
|
|
102
|
-
export default server('
|
|
103
|
-
tool('
|
|
104
|
-
description: '
|
|
105
|
-
input: z.object({
|
|
106
|
-
output: z.object({
|
|
138
|
+
export default server('todo', { title: 'Tasks', version: '1.0.0', use: { tasks } }, [
|
|
139
|
+
tool('find_tasks', {
|
|
140
|
+
description: 'Find tasks whose text matches a query — call this first to resolve a task the user names by text into its id, then pass that id to complete_task.',
|
|
141
|
+
input: z.object({ query: z.string() }),
|
|
142
|
+
output: z.object({ matches: z.array(z.object({ id: z.string(), title: z.string() })) }),
|
|
143
|
+
fulfil: ({ input, connectors }) => {
|
|
144
|
+
const found = connectors.tasks.search_tasks({ query: input.query });
|
|
145
|
+
return { matches: found.matches };
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
tool('complete_task', {
|
|
149
|
+
description: 'Mark a task complete by its id (get the id from find_tasks).',
|
|
150
|
+
input: z.object({ id: z.string() }),
|
|
151
|
+
output: z.object({ ok: z.boolean() }),
|
|
107
152
|
fulfil: ({ input, connectors }) => {
|
|
108
|
-
const
|
|
109
|
-
return {
|
|
153
|
+
const result = connectors.tasks.close_task({ id: input.id });
|
|
154
|
+
return { ok: result.ok };
|
|
110
155
|
},
|
|
111
156
|
}),
|
|
112
157
|
]);
|
|
113
158
|
```
|
|
114
159
|
|
|
115
|
-
|
|
160
|
+
The model never sees a task id from the user; `find_tasks` returns `{ id, title }` summaries it can pick from, then `complete_task` acts by id. Keep write actions (`complete_task`) separate and explicitly described so the host can gate them.
|
|
116
161
|
|
|
117
162
|
## Compute connector example
|
|
118
163
|
|
|
@@ -120,10 +165,10 @@ More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `clientCredentials`
|
|
|
120
165
|
const scoring = connector('scoring').version('1.0.0').compute('normalize', {
|
|
121
166
|
input: { email: { type: "string", required: true }, priority: { type: "string" } },
|
|
122
167
|
output: { score: { type: "number", required: true } },
|
|
123
|
-
calls: {
|
|
168
|
+
calls: { find_customer: 'crm.find_customer' },
|
|
124
169
|
limits: { timeoutMs: 1000, maxHostCalls: 2 },
|
|
125
170
|
run(input, { callOperation }) {
|
|
126
|
-
const customer = callOperation("
|
|
171
|
+
const customer = callOperation("find_customer", { email: input.email }) as { id?: string };
|
|
127
172
|
return { score: customer.id && input.priority === "high" ? 100 : 50 };
|
|
128
173
|
},
|
|
129
174
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# ChatGPT App compliance (pre-submission)
|
|
2
|
+
|
|
3
|
+
`noodle check --target chatgpt` verifies the *metadata* prerequisites; app-store submission also faces a
|
|
4
|
+
human review against OpenAI’s Apps SDK UX principles. Run this checklist against the built app before
|
|
5
|
+
submitting, and render it as an audit table in the design wireframe (`design/wireframe.html` in the
|
|
6
|
+
`acme-*` examples) so partners and reviewers see it up front.
|
|
7
|
+
|
|
8
|
+
## Contents
|
|
9
|
+
|
|
10
|
+
- Metadata gate vs review
|
|
11
|
+
- Pre-submission checklist
|
|
12
|
+
- UI guidelines
|
|
13
|
+
- Domain guardrails
|
|
14
|
+
- Privacy and data
|
|
15
|
+
|
|
16
|
+
## Metadata gate vs review
|
|
17
|
+
|
|
18
|
+
`noodle check --target chatgpt --json` returning `ok:true` means the widget is *metadata-ready* (widget
|
|
19
|
+
`domain`, `openai/outputTemplate`, CSP, tool annotations, and `invoking`/`invoked` invocation copy are
|
|
20
|
+
present) — it does NOT prove host rendering, conversation UX, or submission acceptance. Validate real
|
|
21
|
+
rendering in ChatGPT Developer Mode / MCP Inspector, then run the checklist below.
|
|
22
|
+
|
|
23
|
+
## Pre-submission checklist (what review looks for)
|
|
24
|
+
|
|
25
|
+
1. **Conversational value** — at least one capability relies on ChatGPT’s strengths: natural-language
|
|
26
|
+
actions no tap-driven app can do (e.g. "two margheritas and a lemon tart" parses into a cart). Cite
|
|
27
|
+
concrete app behavior, not aspirations.
|
|
28
|
+
2. **Beyond base ChatGPT** — new knowledge, actions, or presentation (grounded partner data, live
|
|
29
|
+
inventory, signed handoffs, real-world routing).
|
|
30
|
+
3. **Atomic, model-friendly actions** — self-contained tools with explicit input/output schemas, and an
|
|
31
|
+
annotation on every tool (`annotations.readOnly()` / `.action()` / `.openAction()`).
|
|
32
|
+
4. **Helpful UI only** — justify each widget (would plain text degrade UX?), and note what you
|
|
33
|
+
deliberately did NOT build a widget for (payment is off-app → no payment widget).
|
|
34
|
+
5. **In-chat task completion** — the user finishes a meaningful task in chat. For a top-of-funnel app,
|
|
35
|
+
the task is the discovery/config loop completed in-chat with an intentional handoff.
|
|
36
|
+
6. **Performance** — tool calls scoped per step; response-time targets stated.
|
|
37
|
+
7. **Discoverability** — broad, natural trigger prompts listed; description keywords planned. Golden
|
|
38
|
+
prompt sets and metadata optimization are a launch workstream, not polish.
|
|
39
|
+
8. **Platform fit** — multi-turn dialogue, conversation memory, and multimodality where genuinely useful.
|
|
40
|
+
|
|
41
|
+
## UI guidelines
|
|
42
|
+
|
|
43
|
+
System fonts, monochrome outlined icons, WCAG AA contrast, at most two actions on inline cards, no nested
|
|
44
|
+
scroll, and the right display mode per intent (inline by default; fullscreen only where browsing needs
|
|
45
|
+
it; picture-in-picture only for live state). Brand only through `server` `branding` tokens — accent on
|
|
46
|
+
the primary CTA, logo, and badges, nothing else; the compiler derives the palette. Never inject raw
|
|
47
|
+
global CSS.
|
|
48
|
+
|
|
49
|
+
## Domain guardrails
|
|
50
|
+
|
|
51
|
+
For regulated-adjacent apps, add app-specific trust behaviors and **show them in the rendered pixels**:
|
|
52
|
+
cite the source and its revision for consequential lookups; frame regulated content as "considerations,
|
|
53
|
+
not a ruling"; never invent compatibility, availability, or pricing; and always show the relevant
|
|
54
|
+
caution/disclaimer. These are what make a regulated-adjacent app approvable.
|
|
55
|
+
|
|
56
|
+
## Privacy and data
|
|
57
|
+
|
|
58
|
+
Data flows through OpenAI; tool payloads and whatever the server stores must match the partner’s privacy
|
|
59
|
+
policy. No payment happens in chat (PCI stays off-app). Avoid per-user OAuth in a top-of-funnel v1 (use
|
|
60
|
+
service credentials via a `connector`); add end-user auth only for two-way apps (`customerAuth`). Keep
|
|
61
|
+
secrets out of tool output, widgets, and logs. If the partner’s published policy predates the app, flag a
|
|
62
|
+
privacy gap for their counsel before submission. Re-run this checklist against the *built* app before
|
|
63
|
+
every submission — not just the wireframe.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Connect a live API (you were given a key)
|
|
2
|
+
|
|
3
|
+
When the user hands you an API key or credentials, don't infer the data from documentation — docs
|
|
4
|
+
drift. Probe the live API, learn the real shape, then encode it as a `connector`. The loop:
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- Secure the key first
|
|
9
|
+
- Probe the live API
|
|
10
|
+
- Model the connector from the observed shape
|
|
11
|
+
- Design intent tools
|
|
12
|
+
- Prove real output
|
|
13
|
+
- Then build the app
|
|
14
|
+
|
|
15
|
+
## Secure the key first
|
|
16
|
+
|
|
17
|
+
Never inline or log the key. Have the user put it in an environment variable, then store it as a
|
|
18
|
+
managed secret and reference it only as `secret(...)`:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
export SOME_API_KEY=… # the user sets this; it never appears in a file or prompt
|
|
22
|
+
noodle secrets set SOME_API_KEY --from-env SOME_API_KEY
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
In `server.ts` the key is only ever `secret("SOME_API_KEY")` — keep the raw value out of code, tests,
|
|
26
|
+
prompts, logs, and generated files.
|
|
27
|
+
|
|
28
|
+
## Probe the live API
|
|
29
|
+
|
|
30
|
+
Learn the actual response shape empirically. Two ways — capture one real example response per endpoint
|
|
31
|
+
you will use, and read its field names, nesting, array shapes, pagination, and id-vs-label fields:
|
|
32
|
+
|
|
33
|
+
- **With your own HTTP/shell tool** — call a representative read endpoint using the key **from the env
|
|
34
|
+
var**, never the literal (so it stays out of logs): `curl -H "Authorization: Bearer $SOME_API_KEY"
|
|
35
|
+
https://api.example.com/things`. Inspect the returned JSON.
|
|
36
|
+
- **Noodle-native** — author a minimal read operation that maps the whole body (`response: { raw:
|
|
37
|
+
'${response}' }`), `noodle secrets set` the key, then `noodle tools call` it to see the real payload
|
|
38
|
+
in-process.
|
|
39
|
+
|
|
40
|
+
## Model the connector from the observed shape
|
|
41
|
+
|
|
42
|
+
Encode the API as an HTTP connector, mapping only the fields you actually saw into a small typed
|
|
43
|
+
`output`:
|
|
44
|
+
|
|
45
|
+
- `connector("id").version("1.0.0").http({ baseUrl, allowedOrigins, auth, operations })`.
|
|
46
|
+
- `auth: { kind: 'bearer', secret: secret('SOME_API_KEY') }` — or `{ kind: 'apiKey', header: 'X-API-Key',
|
|
47
|
+
secret: secret('SOME_API_KEY') }`. Never put the credential in operation `headers`.
|
|
48
|
+
- Per operation: `method`, `path` (with `{id}` templates), `query: ["arg"]` for URL params, `input`,
|
|
49
|
+
`output`, and a `response` mapping whose `${response.path}` matches the real JSON — the parsed body is
|
|
50
|
+
bound directly to `${response}` (no `.body` envelope); use bracket indices for arrays
|
|
51
|
+
(`${response.results[0].id}`).
|
|
52
|
+
|
|
53
|
+
The full connector shape, every `auth.kind`, and compute connectors are in
|
|
54
|
+
`references/authoring-workflow.md`.
|
|
55
|
+
|
|
56
|
+
## Design intent tools
|
|
57
|
+
|
|
58
|
+
Shape tools around what the user says, not 1:1 around endpoints. Pair an id-taking action with a
|
|
59
|
+
find/search operation that returns `{ id, label }` summaries so the model resolves text → id itself,
|
|
60
|
+
and map each response to a few labelled fields the model can speak from. See the "Design tools for the
|
|
61
|
+
model" section of `references/authoring-workflow.md`.
|
|
62
|
+
|
|
63
|
+
## Prove real output
|
|
64
|
+
|
|
65
|
+
`noodle validate` / `noodle test` prove a connector tool *compiles and registers* — not that its
|
|
66
|
+
mapping returns data. With the secret set, run a live read: `noodle tools call <read_tool> --args
|
|
67
|
+
'{…}'` executes the connector against the real API in-process. Confirm the mapped fields are populated,
|
|
68
|
+
not `undefined`; if they are empty, fix the `${response…}` paths against the real payload and re-run.
|
|
69
|
+
Only run a live write if it is safe or the user approved it.
|
|
70
|
+
|
|
71
|
+
## Then build the app
|
|
72
|
+
|
|
73
|
+
With real data flowing, design the experience (`references/experience-design.md`), add widgets where a
|
|
74
|
+
UI genuinely helps (`references/widgets-and-apps.md`), and verify with `noodle check`. Deploy per
|
|
75
|
+
`references/deploy-and-ops.md`, and set the same secret in the hosted environment with `noodle secrets
|
|
76
|
+
set` before the first hosted call.
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# Examples
|
|
2
2
|
|
|
3
|
-
Flagship examples (one per capability). Read the matching example for the pattern you need; extend an existing flagship rather than inventing a new shape.
|
|
3
|
+
Flagship examples (one per capability). Each lives in the Noodle Seed repository under `examples/<name>/` (`server.ts`, `src/views/*.tsx`, `README.md`, and a `design/` folder for the design-first flagships). Read the matching example for the pattern you need; extend an existing flagship rather than inventing a new shape.
|
|
4
4
|
|
|
5
5
|
| Example | Use when |
|
|
6
6
|
| :-- | :-- |
|
|
7
7
|
| `hello` | Minimal TypeScript quickstart — a single tool, no connectors/widgets. |
|
|
8
8
|
| `weather` | HTTP connectors, multi-step flows, and the sandboxed compute connector. |
|
|
9
9
|
| `food-ordering` | Consumer ordering MCP App widgets, app-only helpers, cart state, assets, branding, and handoff. |
|
|
10
|
+
| `acme-discovery` | Top-of-funnel discovery→handoff: a discovery carousel, a `create_handoff` deep link, and a design-first UX spec + wireframe. |
|
|
11
|
+
| `acme-tasks` | A two-way productivity app designed around its top-3 prioritized flows (capture/prioritize/complete), with a design-first flow spec + wireframe. |
|
|
12
|
+
| `acme-bistro` | End-to-end ordering with a payment-only handoff; ships a gold-standard `design/` set (UX doc, wireframe with compliance audit, API contract). |
|
|
10
13
|
| `customer-auth` | End-user (customer) auth via OIDC/Firebase bridge with delegated credentials. |
|
|
11
14
|
| `stateful-draft` | Durable, caller-scoped widget state handles with optimistic revisions. |
|
|
12
15
|
| `perplexity` | A real SaaS API with bearer auth and a managed `secret`. |
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Experience design
|
|
2
|
+
|
|
3
|
+
Design the app experience before you author it. A ChatGPT app is a conversation with tools and
|
|
4
|
+
widgets, not a website — decide the funnel, the tools, the widgets, and the handoff first, then
|
|
5
|
+
build with the SDK (`references/authoring-workflow.md`, `references/widgets-and-apps.md`). This
|
|
6
|
+
reference is the design discipline; the build references are the mechanics.
|
|
7
|
+
|
|
8
|
+
## Contents
|
|
9
|
+
|
|
10
|
+
- Design first
|
|
11
|
+
- The handoff is the product
|
|
12
|
+
- Grounded, never guessing
|
|
13
|
+
- Two users: the human and the model
|
|
14
|
+
- ChatGPT-native surface
|
|
15
|
+
- Scope discipline and auth stance
|
|
16
|
+
- Wireframe and UX-spec anatomy
|
|
17
|
+
- The deliverables
|
|
18
|
+
- From design to build
|
|
19
|
+
|
|
20
|
+
## Design first
|
|
21
|
+
|
|
22
|
+
Write a short design spec before you author `server.ts` — a `SPEC.md`-style note you keep updated as
|
|
23
|
+
the app evolves. It has required fields:
|
|
24
|
+
|
|
25
|
+
- **Funnel boundary** — one sentence stating exactly what happens in ChatGPT and what happens off-app.
|
|
26
|
+
- **Tools** — each model-visible tool, snake_case, with what it takes and returns.
|
|
27
|
+
- **Widgets and display modes** — each widget, PascalCase, and the display mode it renders in
|
|
28
|
+
(inline card / carousel / fullscreen / picture-in-picture). State the modes you deliberately do
|
|
29
|
+
*not* use, and why.
|
|
30
|
+
- **Grounding sources** — where each fact comes from (a `connector` operation, a provided dataset).
|
|
31
|
+
- **Handoff domains** — the external origins the app links out to (these become server
|
|
32
|
+
`handoff.allowedDomains`).
|
|
33
|
+
- **Exceptions** — any deliberate departure from the defaults below, each with its reason.
|
|
34
|
+
|
|
35
|
+
Then author, preview with `noodle dev` / `noodle devtools`, and iterate. The spec is the contract the
|
|
36
|
+
tools, widgets, and handoff all trace back to.
|
|
37
|
+
|
|
38
|
+
## The handoff is the product
|
|
39
|
+
|
|
40
|
+
Default scope is **top-of-funnel**: ChatGPT owns discovery, intent-shaping, and configuration; the
|
|
41
|
+
partner's own platform owns the transaction, the account, and everything after. The app hands off with
|
|
42
|
+
a deep link that carries the configured state (a pre-filled cart, a chosen configuration), and the
|
|
43
|
+
external target is declared in the server-level `handoff.allowedDomains`. This is a feature: it keeps
|
|
44
|
+
payment and account burden off the app and keeps the partner's platform central.
|
|
45
|
+
|
|
46
|
+
Deliberate exceptions exist — an app that completes the transaction in-chat (handing off for payment
|
|
47
|
+
only), or a two-way app that reads and writes a connected account. When you break the top-of-funnel
|
|
48
|
+
default, write down why in the design spec.
|
|
49
|
+
|
|
50
|
+
## Grounded, never guessing
|
|
51
|
+
|
|
52
|
+
The app's credibility is that it answers from the partner's own data, reached through a `connector`,
|
|
53
|
+
never invented. Never fabricate compatibility, availability, pricing, or eligibility. For
|
|
54
|
+
consequential lookups, **cite the source and its revision in the widget** (e.g. a spec-sheet name and
|
|
55
|
+
revision date) and route unknowns to a human path rather than improvising. Guardrails belong in the
|
|
56
|
+
rendered pixels, not just in prose.
|
|
57
|
+
|
|
58
|
+
## Two users: the human and the model
|
|
59
|
+
|
|
60
|
+
Every tool and widget serves two users at once — the human who interacts and the model that reads the
|
|
61
|
+
result. Keep tools atomic with inputs the model can fill from natural language, and return enough
|
|
62
|
+
structure that the model can speak confidently in one round-trip. Build a widget only where plain text
|
|
63
|
+
would genuinely degrade the experience (photos, comparisons, carts, configuration). If text would do,
|
|
64
|
+
don't build the widget.
|
|
65
|
+
|
|
66
|
+
## ChatGPT-native surface
|
|
67
|
+
|
|
68
|
+
Widgets should read as native to the host. Brand through the `server` `branding` tokens only — the
|
|
69
|
+
compiler derives the palette — and reserve the accent for the primary CTA, the logo, and badges. No
|
|
70
|
+
brand gradients, no app-shoved-into-chat. Keep inline cards to at most two actions and avoid nested
|
|
71
|
+
scroll. Pick each display mode deliberately (fullscreen only where browsing genuinely needs it;
|
|
72
|
+
picture-in-picture only for live ongoing state). See `references/widgets-and-apps.md` for the widget
|
|
73
|
+
mechanics — do not restyle with raw global CSS.
|
|
74
|
+
|
|
75
|
+
## Scope discipline and auth stance
|
|
76
|
+
|
|
77
|
+
Lock the funnel boundary in writing before designing, and keep a "future enhancements" list as the
|
|
78
|
+
pressure valve. No payment happens in chat. Avoid per-user auth in a top-of-funnel v1 — use the
|
|
79
|
+
partner's service credentials via a `connector`; add end-user auth only when the app is two-way by
|
|
80
|
+
nature (see the `customer-auth` example in `references/examples.md`).
|
|
81
|
+
|
|
82
|
+
## Wireframe and UX-spec anatomy
|
|
83
|
+
|
|
84
|
+
For anything non-trivial, sketch the experience as a wireframe before authoring — a walk through the
|
|
85
|
+
conversation, screen by screen. Each screen shows a real user message, the **tool call that precedes
|
|
86
|
+
the widget**, and the widget filled with plausible, internally consistent data (never lorem ipsum).
|
|
87
|
+
Label each widget with its component name so the wireframe, the spec, and the code share one
|
|
88
|
+
vocabulary; put the funnel boundary at the top; and render off-app destinations distinctly (they are
|
|
89
|
+
reached only after the handoff). Each wireframe screen maps directly to noodle: a screen with a widget
|
|
90
|
+
is a `toolWithWidget` + a React `view`; a plain answer is a `tool`; an off-app destination is a
|
|
91
|
+
`handoff.allowedDomains` entry. A compact single screen, anonymized to a fictional "Acme" business:
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
<div class="phone">
|
|
95
|
+
<div class="chatgpt-header">ChatGPT · Acme</div>
|
|
96
|
+
<div class="msg user">14kW array, ~40kWh battery — what pairs with the Acme X?</div>
|
|
97
|
+
<!-- tool call precedes every widget; the model fills inputs from the message -->
|
|
98
|
+
<div class="tool-call">check_compatibility { model: "Acme X", battery_kwh: 40 }</div>
|
|
99
|
+
<div class="wcard">
|
|
100
|
+
<div class="wcard-head">CompatibilityCard</div> <!-- component name = code + spec -->
|
|
101
|
+
<div class="wcard-body">
|
|
102
|
+
<div class="kv">Acme X ⇄ 40kWh pack <span class="badge certified">Compatible</span></div>
|
|
103
|
+
<div class="cite">Source: Acme Integration Guide, Rev 7 (2026)</div> <!-- grounded + cited -->
|
|
104
|
+
<a class="cta">Configure system</a> <!-- ≤2 actions on an inline card -->
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Expand from one screen to the full journey: entry → configuration → handoff, ending at the off-app
|
|
111
|
+
destination. Verify the built app against the design with `noodle check --target chatgpt`.
|
|
112
|
+
|
|
113
|
+
For the full quality bar, open a shipped gold-standard set: in the Noodle Seed repository, the
|
|
114
|
+
`design/` folder of the `acme-discovery` (top-of-funnel), `acme-tasks` (two-way), and `acme-bistro`
|
|
115
|
+
(end-to-end) examples each holds a house-style UX Document and a single-file HTML wireframe with an
|
|
116
|
+
embedded OpenAI Apps SDK compliance audit. Match that bar.
|
|
117
|
+
|
|
118
|
+
## The deliverables
|
|
119
|
+
|
|
120
|
+
The design phase produces up to three artifacts — worked gold-standard versions live in each
|
|
121
|
+
`acme-*` example's `design/` folder (copy their structure, swap the content):
|
|
122
|
+
|
|
123
|
+
- **UX Document** — the thinking artifact. House-style sections: product overview / knowledge base,
|
|
124
|
+
competitive landscape, personas, conversational flow (with tool-call playscripts), widget specs +
|
|
125
|
+
a display-mode strategy, tool definitions, conversation guardrails, journey map, handoff/auth
|
|
126
|
+
architecture, demo scope, success metrics, and future enhancements — opening on the funnel-boundary
|
|
127
|
+
line every scope debate resolves against.
|
|
128
|
+
- **Wireframe** — the single-file HTML alignment artifact (anatomy above) with the embedded compliance
|
|
129
|
+
audit; see `references/chatgpt-compliance.md`.
|
|
130
|
+
- **API contract** — when the partner's backend must be built or wrapped. Escalate: (1) the MCP
|
|
131
|
+
tool→call-sequence map (always); (2) "Recommended API Shapes" — concrete request/response JSON per
|
|
132
|
+
tool, including the hardest nested case; (3) a full OpenAPI spec for transactional apps. Contract
|
|
133
|
+
rules: shape responses for one-round-trip rendering (embed related objects, pre-sort); put stable
|
|
134
|
+
ids on anything the user picks; keep validation in the API, not the model; the handoff endpoint
|
|
135
|
+
mints a signed, expiring URL + attribution and never proxies payment; use server-side partner
|
|
136
|
+
credentials for v1 (per-user auth only for two-way apps); name tools for user intent.
|
|
137
|
+
|
|
138
|
+
## From design to build
|
|
139
|
+
|
|
140
|
+
Once the design spec is settled, build it: `references/authoring-workflow.md` for the author→validate
|
|
141
|
+
loop and connectors, `references/widgets-and-apps.md` for widgets and CSP, `references/examples.md`
|
|
142
|
+
for a flagship to extend, and `references/deploy-and-ops.md` to ship.
|
|
@@ -25,6 +25,6 @@ For protocol/conformance checks, the headless harness is `@mcpjam/cli`, not a `n
|
|
|
25
25
|
| `noodle validate` passes but React views fail to bundle (“requires Vite”) | Project dependencies are not installed — widget bundling uses the app-local Vite | Run the project’s package install, then retry `noodle dev` / `noodle deploy` |
|
|
26
26
|
| Hosted endpoint returns 401 to probes | Expected: hosted servers challenge unauthenticated calls with OAuth metadata | Sign in from the host when prompted; widen who may call with `noodle access set` if testers are outside the org |
|
|
27
27
|
| Tools error only after deploy | Runtime/config differences surface hosted (secrets, connector reachability) | Run `noodle smoke`, then `noodle metrics --agent-output` and `noodle events --tool <name> --status tool_error --json`; check `noodle secrets list` scope |
|
|
28
|
-
| A connector tool validates and lists, but returns empty or `undefined` fields | The `response` mapping references a path the API does not return — usually the wrong root (
|
|
28
|
+
| A connector tool validates and lists, but returns empty or `undefined` fields | The `response` mapping references a path the API does not return — usually the wrong root (a `.body` segment, when the parsed body is bound directly to `${response}`) or the wrong shape | Run `noodle tools call <name> --args <json>` with the secret set and compare the mapped result to the API’s real JSON; map from `${response.<path>}` (the body is `${response}`, there is no `.body`) and use bracket array indices (`${response.items[0].id}`) |
|
|
29
29
|
| Need to invoke a tool from the terminal | Local tools run in-process; the `noodle` CLI is not a general MCP client for **deployed** URLs (there is no `call <url>` verb) | Locally, `noodle tools call <name> --args <json>` (also `noodle resources read` / `noodle prompts get`) runs the tool against the in-process runtime — with the secret set it executes the connector against the real API, so use it to prove mapped output. For a **deployed** URL use MCP Inspector or `npx @mcpjam/cli@latest tools call --url <url> ...` |
|
|
30
30
|
| One customer/session reports a bad answer or protocol error | The failure may be a model/tool error, host protocol error, or connector/runtime error | Run `noodle metrics --agent-output`, then `noodle events --tool <name> --status tool_error --json`; copy the `sessionId` into `noodle events --session <id> --json`, then match timestamps with `noodle logs` |
|