@noodleseed/agent-kit 0.12.0 → 0.13.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/manifest.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
- "packageVersion": "0.12.0",
2
+ "packageVersion": "0.13.0",
3
3
  "files": [
4
4
  {
5
5
  "path": "skills/codex/SKILL.md",
6
- "sha256": "0b986b87d14e9eed8add2c3d294a9c3df319847f410340815d9f4690af126c11",
6
+ "sha256": "4dfd82f9f2acf59f7df30617b8f319b8cc0a743c721aeb027e454cb1af0f1c0a",
7
7
  "agentTarget": "codex"
8
8
  },
9
9
  {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  {
30
30
  "path": "skills/codex/references/authoring-workflow.md",
31
- "sha256": "aba1853b5709cc40f341f827bae739e11142b2a6996872757b1db68e0cb35015",
31
+ "sha256": "eca8d1f2b98ee64f6beda4d983f0f0df9023ceed8bd00ffb9dd6b5025dcaa4e4",
32
32
  "agentTarget": "codex"
33
33
  },
34
34
  {
@@ -43,7 +43,7 @@
43
43
  },
44
44
  {
45
45
  "path": "skills/codex/references/troubleshooting.md",
46
- "sha256": "fe13045fab55fc660c50482540e89654bac6a925c3d189fa3e9650d7e656b7ba",
46
+ "sha256": "834d2c6626dc90c009aebac3ee7ff8977a1f97dfb66cf5b955fd6414e6e2a897",
47
47
  "agentTarget": "codex"
48
48
  },
49
49
  {
@@ -63,7 +63,7 @@
63
63
  },
64
64
  {
65
65
  "path": "skills/claude-code/SKILL.md",
66
- "sha256": "909001bcf63868d51228d2c55da471c12f770b83f15958bbb9eefedb6afd0ec9",
66
+ "sha256": "6027e2822f6045e36955996c3757370cc287966493ef3bb0dd56420eeeb47f1e",
67
67
  "agentTarget": "claude-code"
68
68
  },
69
69
  {
@@ -88,7 +88,7 @@
88
88
  },
89
89
  {
90
90
  "path": "skills/claude-code/references/authoring-workflow.md",
91
- "sha256": "aba1853b5709cc40f341f827bae739e11142b2a6996872757b1db68e0cb35015",
91
+ "sha256": "eca8d1f2b98ee64f6beda4d983f0f0df9023ceed8bd00ffb9dd6b5025dcaa4e4",
92
92
  "agentTarget": "claude-code"
93
93
  },
94
94
  {
@@ -103,7 +103,7 @@
103
103
  },
104
104
  {
105
105
  "path": "skills/claude-code/references/troubleshooting.md",
106
- "sha256": "fe13045fab55fc660c50482540e89654bac6a925c3d189fa3e9650d7e656b7ba",
106
+ "sha256": "834d2c6626dc90c009aebac3ee7ff8977a1f97dfb66cf5b955fd6414e6e2a897",
107
107
  "agentTarget": "claude-code"
108
108
  },
109
109
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noodleseed/agent-kit",
3
- "version": "0.12.0",
3
+ "version": "0.13.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,7 +1,7 @@
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.12.0
4
+ version: 0.13.0
5
5
  hash: dd621716fbd94dab
6
6
  ---
7
7
 
@@ -6,8 +6,8 @@
6
6
  - Fit check
7
7
  - Repair loop
8
8
  - Connectors
9
- - HTTP connector example
10
- - Worked example (full server)
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` templates the outbound call, `response` maps the HTTP body into your typed `output`.
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: [variable('CRM_BASE_URL')],
50
+ allowedOrigins: ['https://api.crm.example'],
51
51
  auth: { kind: 'bearer', secret: secret('CRM_TOKEN') },
52
52
  operations: {
53
- findCustomer: {
53
+ find_customer: {
54
54
  type: 'read',
55
- input: { email: { type: "string", required: true } },
56
- output: { id: { type: "string", required: true }, name: { type: "string" } },
57
- method: "GET",
58
- path: "/customers",
59
- request: { query: { email: "${args.email}" } },
60
- response: { id: "${response.body.data.0.id}", name: "${response.body.data.0.name}" },
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
- createTicket: {
62
+ create_ticket: {
63
63
  type: 'action',
64
- input: { customerId: { type: "string", required: true }, body: { type: "string", required: true } },
65
- output: { ticketId: { type: "string", required: true } },
66
- method: "POST",
67
- path: "/tickets",
68
- request: { body: { customer_id: "${args.customerId}", body: "${args.body}" } },
69
- response: { ticketId: "${response.body.id}" },
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
- Use `${args.field}` for tool/operation inputs and `${response.body.path}` for response mapping. Prefer explicit `allowedOrigins`; OAuth token/session URLs must also be on an allowed origin. 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.
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
- ## Worked example (full server)
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
- Declare the API as data, bind it with `use`, then record a call in a tool. `auth` reads a managed `secret(...)` never inline a key. Map the HTTP body into your typed `output` with `${response...}`.
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 crm = connector('acme_crm')
85
- .version('1.0.0')
86
- .http({
87
- baseUrl: 'https://api.acme.example',
88
- allowedOrigins: ['https://api.acme.example'],
89
- auth: { kind: 'apiKey', header: 'X-Api-Key', secret: secret('ACME_CRM_TOKEN') },
90
- operations: {
91
- get_ticket: {
92
- type: 'read',
93
- method: 'GET',
94
- path: '/v1/tickets/{id}',
95
- input: { id: { type: 'string', required: true } },
96
- output: { subject: { type: 'string' }, status: { type: 'string' } },
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('support', { title: 'Support', version: '1.0.0', use: { crm } }, [
103
- tool('ticket_status', {
104
- description: 'Look up a support ticket by id.',
105
- input: z.object({ ticket_id: z.string() }),
106
- output: z.object({ subject: z.string(), status: z.string() }),
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 ticket = connectors.crm.get_ticket({ id: input.ticket_id });
109
- return { subject: ticket.subject, status: ticket.status };
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
- More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `clientCredentials` | `delegatedOAuth` | `delegatedSessionCookie`. 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.
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: { findCustomer: 'crm.findCustomer' },
168
+ calls: { find_customer: 'crm.find_customer' },
124
169
  limits: { timeoutMs: 1000, maxHostCalls: 2 },
125
170
  run(input, { callOperation }) {
126
- const customer = callOperation("findCustomer", { email: input.email }) as { id?: string };
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
  });
@@ -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 (`${response.body...}` when the parsed body is bound 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}`) |
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` |
@@ -1,7 +1,7 @@
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.12.0
4
+ version: 0.13.0
5
5
  hash: 68c2cc25462f5be7
6
6
  ---
7
7
 
@@ -6,8 +6,8 @@
6
6
  - Fit check
7
7
  - Repair loop
8
8
  - Connectors
9
- - HTTP connector example
10
- - Worked example (full server)
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` templates the outbound call, `response` maps the HTTP body into your typed `output`.
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: [variable('CRM_BASE_URL')],
50
+ allowedOrigins: ['https://api.crm.example'],
51
51
  auth: { kind: 'bearer', secret: secret('CRM_TOKEN') },
52
52
  operations: {
53
- findCustomer: {
53
+ find_customer: {
54
54
  type: 'read',
55
- input: { email: { type: "string", required: true } },
56
- output: { id: { type: "string", required: true }, name: { type: "string" } },
57
- method: "GET",
58
- path: "/customers",
59
- request: { query: { email: "${args.email}" } },
60
- response: { id: "${response.body.data.0.id}", name: "${response.body.data.0.name}" },
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
- createTicket: {
62
+ create_ticket: {
63
63
  type: 'action',
64
- input: { customerId: { type: "string", required: true }, body: { type: "string", required: true } },
65
- output: { ticketId: { type: "string", required: true } },
66
- method: "POST",
67
- path: "/tickets",
68
- request: { body: { customer_id: "${args.customerId}", body: "${args.body}" } },
69
- response: { ticketId: "${response.body.id}" },
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
- Use `${args.field}` for tool/operation inputs and `${response.body.path}` for response mapping. Prefer explicit `allowedOrigins`; OAuth token/session URLs must also be on an allowed origin. 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.
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
- ## Worked example (full server)
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
- Declare the API as data, bind it with `use`, then record a call in a tool. `auth` reads a managed `secret(...)` never inline a key. Map the HTTP body into your typed `output` with `${response...}`.
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 crm = connector('acme_crm')
85
- .version('1.0.0')
86
- .http({
87
- baseUrl: 'https://api.acme.example',
88
- allowedOrigins: ['https://api.acme.example'],
89
- auth: { kind: 'apiKey', header: 'X-Api-Key', secret: secret('ACME_CRM_TOKEN') },
90
- operations: {
91
- get_ticket: {
92
- type: 'read',
93
- method: 'GET',
94
- path: '/v1/tickets/{id}',
95
- input: { id: { type: 'string', required: true } },
96
- output: { subject: { type: 'string' }, status: { type: 'string' } },
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('support', { title: 'Support', version: '1.0.0', use: { crm } }, [
103
- tool('ticket_status', {
104
- description: 'Look up a support ticket by id.',
105
- input: z.object({ ticket_id: z.string() }),
106
- output: z.object({ subject: z.string(), status: z.string() }),
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 ticket = connectors.crm.get_ticket({ id: input.ticket_id });
109
- return { subject: ticket.subject, status: ticket.status };
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
- More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `clientCredentials` | `delegatedOAuth` | `delegatedSessionCookie`. 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.
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: { findCustomer: 'crm.findCustomer' },
168
+ calls: { find_customer: 'crm.find_customer' },
124
169
  limits: { timeoutMs: 1000, maxHostCalls: 2 },
125
170
  run(input, { callOperation }) {
126
- const customer = callOperation("findCustomer", { email: input.email }) as { id?: string };
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
  });
@@ -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 (`${response.body...}` when the parsed body is bound 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}`) |
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` |