@noodleseed/one 0.158.0 → 0.159.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.
@@ -1 +1 @@
1
- {"version":3,"file":"openapi-import.d.ts","sourceRoot":"","sources":["../src/openapi-import.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,oBAAoB,GAAG;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC,CAgCA"}
1
+ {"version":3,"file":"openapi-import.d.ts","sourceRoot":"","sources":["../src/openapi-import.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,oBAAoB,GAAG;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC,CAmCA"}
@@ -1,9 +1,9 @@
1
- import { readFileSync } from 'node:fs';
2
- import { parseOpenApiToIr, title, } from '@noodle-borg/openapi-import';
1
+ import { closeSync, constants, fstatSync, openSync, readSync } from 'node:fs';
2
+ import { MAX_OPENAPI_SOURCE_BYTES, parseOpenApiToIr, title, } from '@noodle-borg/openapi-import';
3
3
  import { slug } from './deploy.js';
4
4
  import { importedProjectFiles, writeImportedProject } from './import-scaffold.js';
5
5
  export function importOpenApiProject(options) {
6
- const ir = parseOpenApiToIr(readFileSync(options.specPath, 'utf8'), {
6
+ const ir = parseOpenApiToIr(readOpenApiSource(options.specPath), {
7
7
  name: options.name,
8
8
  ...(options.baseUrl !== undefined ? { baseUrl: options.baseUrl } : {}),
9
9
  });
@@ -23,7 +23,10 @@ npm exec -- noodle agents setup --apply
23
23
 
24
24
  The generated test proves the offline contract compiles, not live backend behavior. Review each
25
25
  action before calling it; writes require explicit confirmation. Add a sandbox fixture for one
26
- representative operation before deployment. Unsupported request bodies require manual mapping.
26
+ representative operation before deployment. Supported JSON request bodies are typed under \`input.body\`
27
+ and sent unchanged; required fields, optional bodies, arrays, integers and object extras survive import.
28
+ Unsupported encodings or constraints stop import before writing; use the connector guide to author them:
29
+ https://docs.noodleseed.dev/docs/guides/connectors
27
30
  ${ir.secretRefs.length
28
31
  ? `\nSet ${ir.secretRefs.map((name) => `\`${name}\``).join(', ')} locally in ignored \`.env\` or \`.env.noodle\`.
29
32
  For hosted deployment, use \`noodle secrets set <NAME>\` interactively; never paste values into
@@ -35,15 +38,8 @@ commands, source, logs or agent context.\n`
35
38
  }
36
39
  function renderTypeScriptProject(ir) {
37
40
  const base = new URL(ir.baseUrl);
38
- if (!['https:', 'http:'].includes(base.protocol) ||
39
- base.username ||
40
- base.password ||
41
- base.search ||
42
- base.hash) {
43
- throw new Error('import openapi: use an HTTP(S) base URL without credentials, query or fragment');
44
- }
45
41
  const contracts = ir.operations.map((operation) => `${key(operation.safeName)}: {
46
- input: ${zodInputObject(operation.parameters)},
42
+ input: ${zodInputObject(operation)},
47
43
  output: z.object({ value: ${operation.output === undefined ? 'z.unknown()' : zodSource(operation.output)} }),
48
44
  }`);
49
45
  const operations = ir.operations.map((operation) => {
@@ -54,13 +50,17 @@ function renderTypeScriptProject(ir) {
54
50
  path: ${quote(operation.connectorPath)},
55
51
  input: contracts[${quote(operation.safeName)}].input,
56
52
  ${query}
53
+ ${operation.requestBody === undefined ? '' : "request: '${args.body}',"}
57
54
  output: contracts[${quote(operation.safeName)}].output,
58
55
  response: { value: '\${response}' },
59
56
  }`;
60
57
  });
61
58
  const tools = ir.operations.map((operation) => {
62
- const callArgs = operation.parameters
63
- .map((param) => `${key(param.name)}: input[${quote(param.name)}]`)
59
+ const callArgs = [
60
+ ...operation.parameters.map((param) => param.name),
61
+ ...(operation.requestBody === undefined ? [] : ['body']),
62
+ ]
63
+ .map((name) => `${key(name)}: input[${quote(name)}]`)
64
64
  .join(', ');
65
65
  return `tool(${quote(operation.safeName)}, {
66
66
  description: ${quote(operation.description)},
@@ -102,13 +102,14 @@ export default server(${quote(ir.connectorId)}, {
102
102
  }
103
103
  const IDENTIFIER_KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
104
104
  /** Renders a parameter list as `z.object({ ... })` source with per-parameter lossless Zod. */
105
- function zodInputObject(parameters) {
106
- if (parameters.length === 0)
105
+ function zodInputObject(operation) {
106
+ if (operation.parameters.length === 0 && operation.requestBody === undefined)
107
107
  return 'z.object({})';
108
- const shape = parameters
109
- .map((param) => `${key(param.name)}: ${zodParameterSource(param.schema)}${param.required ? '' : '.optional()'}`)
110
- .join(', ');
111
- return `z.object({ ${shape} }).strict()`;
108
+ const properties = operation.parameters.map((param) => `${key(param.name)}: ${zodParameterSource(param.schema)}${param.schema.format === undefined ? '' : `.meta({ format: ${quote(param.schema.format)} })`}${param.required ? '' : '.optional()'}`);
109
+ if (operation.requestBody !== undefined) {
110
+ properties.push(`body: ${zodSource(operation.requestBody.schema)}${operation.requestBody.required ? '' : '.optional()'}`);
111
+ }
112
+ return `z.object({ ${properties.join(', ')} }).strict()`;
112
113
  }
113
114
  /**
114
115
  * Renders one imported parameter schema as Zod source: string/number/boolean map directly,
@@ -143,20 +144,25 @@ function zodSource(schema) {
143
144
  }
144
145
  return `z.string()${nullable}`;
145
146
  case 'number':
146
- return `z.number()${nullable}`;
147
+ return `z.number()${schema.integer === true ? '.int()' : ''}${nullable}`;
147
148
  case 'boolean':
148
149
  return `z.boolean()${nullable}`;
149
150
  case 'array':
150
151
  return `z.array(${zodSource(schema.items)})${nullable}`;
151
152
  case 'object': {
152
- if (schema.properties.length === 0) {
153
+ if (schema.properties.length === 0 && schema.additionalProperties === undefined) {
153
154
  return `z.record(z.string(), z.unknown())${nullable}`;
154
155
  }
155
156
  const properties = schema.properties.map((property) => {
156
157
  const optional = property.required ? '' : '.optional()';
157
158
  return `${key(property.name)}: ${zodSource(property.schema)}${optional}`;
158
159
  });
159
- return `z.object({ ${properties.join(', ')} })${nullable}`;
160
+ const extras = schema.additionalProperties === undefined
161
+ ? ''
162
+ : schema.additionalProperties
163
+ ? '.passthrough()'
164
+ : '.strict()';
165
+ return `z.object({ ${properties.join(', ')} })${extras}${nullable}`;
160
166
  }
161
167
  case 'unknown':
162
168
  return 'z.unknown()';
@@ -170,4 +176,26 @@ function key(value) {
170
176
  return `[${quote(value)}]`;
171
177
  return IDENTIFIER_KEY.test(value) ? value : quote(value);
172
178
  }
179
+ /** Read a regular local input with a fixed allocation, even if its size changes during the read. */
180
+ function readOpenApiSource(path) {
181
+ const descriptor = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK);
182
+ try {
183
+ if (!fstatSync(descriptor).isFile())
184
+ throw new Error('import openapi: spec must be a regular file');
185
+ const bytes = Buffer.alloc(MAX_OPENAPI_SOURCE_BYTES + 1);
186
+ let length = 0;
187
+ while (length < bytes.length) {
188
+ const count = readSync(descriptor, bytes, length, bytes.length - length, length);
189
+ if (count === 0)
190
+ break;
191
+ length += count;
192
+ }
193
+ if (length > MAX_OPENAPI_SOURCE_BYTES)
194
+ throw new Error('import openapi: document exceeds the 6 MiB size limit; import a scoped API document');
195
+ return bytes.subarray(0, length).toString('utf8');
196
+ }
197
+ finally {
198
+ closeSync(descriptor);
199
+ }
200
+ }
173
201
  //# sourceMappingURL=openapi-import.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"openapi-import.js","sourceRoot":"","sources":["../src/openapi-import.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"openapi-import.js","sourceRoot":"","sources":["../src/openapi-import.ts"],"names":[],"mappings":"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"}
@@ -104,7 +104,7 @@ export const BUNDLED_EXAMPLE_FILES = [
104
104
  { relPath: "examples/stateful-draft/test/draft-card.test.tsx", content: "// @vitest-environment happy-dom\nimport { act } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { afterEach, beforeEach, expect, it, vi } from 'vitest';\n\nconst callTool = vi.fn();\nconst followUp = vi.fn();\nconst initial = {\n value: { title: 'Team launch', audience: 'New teammates', goal: 'Complete their first project' },\n revision: 4,\n status: 'active',\n};\nlet entry: unknown = initial;\nvi.mock('../src/helpers.js', () => ({\n useToolInfo: () => ({ structuredContent: entry }),\n useCallTool: () => ({ callTool }),\n useSendFollowUpMessage: () => followUp,\n}));\n\nimport DraftCard from '../src/views/draft-card.js';\n\nlet host: HTMLDivElement;\nlet root: ReturnType<typeof createRoot>;\nbeforeEach(async () => {\n vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);\n callTool.mockReset();\n followUp.mockReset();\n entry = initial;\n host = document.createElement('div');\n document.body.append(host);\n root = createRoot(host);\n await act(async () => root.render(<DraftCard />));\n});\nafterEach(() => {\n act(() => root.unmount());\n host.remove();\n});\nfunction button(label: string) {\n const found = [...host.querySelectorAll('button')].find((entry) => entry.textContent === label);\n if (!found) throw new Error(`Missing button: ${label}`);\n return found;\n}\n\nit('saves using the server revision and displays only the returned result', async () => {\n callTool.mockResolvedValue({ structuredContent: { ...initial, revision: 8 } });\n await act(async () => button('Save brief').click());\n expect(callTool).toHaveBeenCalledWith({ ...initial.value, expectedRevision: 4 });\n expect(host.textContent).toContain('Your brief is saved.');\n expect(button('Continue with an account').disabled).toBe(false);\n});\n\nit('shows a proposed brief without pretending it is already saved', async () => {\n entry = { value: {}, revision: 0, status: 'active', proposal: initial.value };\n await act(async () => root.render(<DraftCard />));\n expect(host.querySelector('input')?.value).toBe('Team launch');\n expect(button('Continue with an account').disabled).toBe(true);\n callTool.mockResolvedValue({ structuredContent: { ...initial, revision: 1 } });\n await act(async () => button('Save brief').click());\n expect(callTool).toHaveBeenCalledWith({ ...initial.value, expectedRevision: 0 });\n});\n\nit('does not invent a save when confirmation is pending or the response is missing', async () => {\n callTool.mockResolvedValue({});\n await act(async () => button('Save brief').click());\n expect(host.textContent).not.toContain('Your brief is saved.');\n expect(host.textContent).toContain('No save was confirmed.');\n});\n\nit('retains edits on a stale write and requires a reload before another save', async () => {\n callTool.mockResolvedValue({ isError: true });\n await act(async () => button('Save brief').click());\n expect(host.querySelector('input')?.value).toBe('Team launch');\n expect(button('Save brief').disabled).toBe(true);\n expect(host.textContent).toContain('Reload saved');\n callTool.mockResolvedValue({ structuredContent: { ...initial, revision: 7 } });\n await act(async () => button('Reload saved').click());\n await act(async () => button('Save brief').click());\n expect(callTool).toHaveBeenLastCalledWith({ ...initial.value, expectedRevision: 7 });\n});\n\nit('keeps continuing separate from saving and makes no project-creation claim', async () => {\n await act(async () => button('Continue with an account').click());\n expect(callTool).not.toHaveBeenCalled();\n expect(followUp).toHaveBeenCalledWith({\n prompt: 'I would like to continue with my saved brief in an account.',\n });\n expect(host.textContent).not.toContain('Project created');\n});\n" },
105
105
  { relPath: "examples/stateful-draft/test/server.test.ts", content: "import { fileURLToPath } from 'node:url';\nimport { validate } from '@noodleseed/one';\nimport { describe, expect, it } from 'vitest';\nimport app from '../src/server.js';\n\ndescribe('stateful draft onboarding reference', () => {\n it('reads and saves authoritative state instead of a widget-only copy', async () => {\n const manifest = await app.toManifest();\n expect(manifest.tools.find((entry) => entry.name === 'open_draft')?.fulfilment.steps).toEqual([\n expect.objectContaining({ use: 'state.read_state', args: { handle: 'draft' } }),\n ]);\n expect(manifest.tools.find((entry) => entry.name === 'save_draft')).toMatchObject({\n annotations: { readOnlyHint: false, confirm: true },\n fulfilment: {\n steps: [\n expect.objectContaining({\n use: 'state.patch_state',\n args: {\n handle: 'draft',\n expectedRevision: '${input.expectedRevision}',\n value: {\n title: '${input.title}',\n audience: '${input.audience}',\n goal: '${input.goal}',\n },\n },\n }),\n ],\n },\n });\n });\n\n it('limits anonymous access and transfers only an expiring draft after verified login', async () => {\n const manifest = await app.toManifest();\n expect(manifest.state?.handles.draft).toMatchObject({\n scope: 'caller',\n ttlSeconds: 86400,\n claimOnAuthentication: true,\n });\n expect(manifest.server.assistant?.surfaces?.map((surface) => surface.mode)).toEqual([\n 'mixed',\n 'authenticated',\n ]);\n const continued = manifest.tools.find((entry) => entry.name === 'continue_draft');\n expect(continued?.annotations?.readOnlyHint).toBe(true);\n expect(continued?.fulfilment.output).toMatchObject({ accountId: '${user.id}' });\n expect(continued?.fulfilment.steps).toEqual([\n expect.objectContaining({ use: 'state.read_state', args: { handle: 'draft' } }),\n ]);\n });\n\n it('compiles through the public validator, including anonymous action confirmation', async () => {\n const result = await validate({\n manifestPath: fileURLToPath(new URL('../src/server.ts', import.meta.url)),\n });\n expect(result.ok, JSON.stringify(result.ok ? [] : result.errors)).toBe(true);\n });\n});\n" },
106
106
  { relPath: "examples/stateful-draft/vitest.config.ts", content: "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n oxc: { jsx: { runtime: 'automatic' } },\n test: { include: ['test/**/*.test.{ts,tsx}'], testTimeout: 30_000, maxWorkers: 2 },\n});\n" },
107
- { relPath: "examples/weather/README.md", content: "# Weather Briefing\n\nTwo declarative tools that show the runtime's breadth working together, with **no auth and no API\nkeys**. The `weather_briefing` tool takes a city name and runs a **three-step flow**:\n\nCapability slots: HTTP connector authoring, ordered fulfilment flows, query/response mapping,\n**list-returning connector output** (a connector that returns a live, variable-length array), and\nsandboxed compute, including an explicit least-privilege per-operation response-size bound.\n\nFor a different API with an OpenAPI document, start with `noodle import openapi <file>` in a separate\ndirectory. Its shared schemas and offline test establish the contract, not live behavior; follow the\n[connector guide](https://docs.noodleseed.dev/docs/guides/connectors) before replacing this curated flow.\n\n1. **`geo.search`** → geocode the city to coordinates (Open-Meteo Geocoding API)\n2. **`forecast.current`** → fetch current weather for those coordinates (Open-Meteo Forecast API)\n3. **`brief.summarize`** → derive a human-readable briefing in a **WASM/QuickJS compute sandbox**\n\nThe second tool, `search_places`, shows a connector returning a **live, variable-length list**: it binds\nthe whole Open-Meteo geocoding `results` array with `${response.results}`, then narrows each match to\n`{ id, label }` in a compute connector — the \"search → a list of options the model can pick from\"\npattern. Narrowing lives in compute because a `${...}` response mapping cannot iterate an array and a\ntool's Zod output does not strip fields at runtime.\n\nIt exercises, in one TypeScript-authored app:\n\n- **Server-level branding** with semantic tokens carried through the runtime artifact for any generated\n app surface.\n- **Ordered flow execution** with outputs threaded between steps (`${steps.geo.latitude}` → next step).\n- **Two HTTP connectors on two different hosts**, each with its own egress allowlist.\n- **Query parameters** (`query: [...]`) and a constant query baked into the path (`?current_weather=true`).\n- **Deep response mapping** with the `${...}` language — single-element indexing\n (`${response.results[0].latitude}`, `${response.current_weather.temperature}`) **and** whole-array\n binding (`${response.results}` returns the entire list verbatim).\n- **A list-returning connector + compute narrowing** — `geo.search_list` binds the whole `results`\n array; `places.narrow` reduces each element to `{ id, label }` and normalizes the no-results case\n to `[]`.\n- **A per-operation transport bound** — `search_list` sets\n `limits: { maxResponseBytes: 256 * 1024 }`, tightening this known-small endpoint below the 1 MiB default.\n The authoring ceiling is 6 MiB, but grant only the bytes representative evidence proves this operation\n needs.\n- **Sandboxed compute** (no network/fs/env/clock) turning raw numbers into conditions + advice.\n- **Typed input/output schemas** emitted as JSON Schema 2020-12.\n\n## APIs that require form-urlencoded search bodies\n\nThe live Open-Meteo calls above are GET requests. For APIs whose search endpoint is a POST expecting\n`application/x-www-form-urlencoded`, keep authoring a request object and select the encoding explicitly:\n\n```ts\nsearch_quotes: {\n type: 'read',\n method: 'POST',\n path: '/quotes/search',\n requestEncoding: 'form-urlencoded',\n input: z.object({\n fromAirportId: z.string(),\n categories: z.array(z.string()),\n }),\n request: {\n 'from airport id': '${args.fromAirportId}',\n 'aircraft[categories]': '${args.categories}',\n },\n // output and response mapping omitted\n},\n```\n\nNoodle builds a `URLSearchParams` body: spaces and punctuation in field names are encoded normally, while\neach array or nested object is JSON-stringified into its individual form field. Do not pre-encode the body\nor set `Content-Type` manually; the connector owns both.\n\n## Run it locally\n\nFrom the repo root, with the workspace built (`pnpm build`):\n\n```bash\n: # 1. boot the local loopback dev server\nnode packages/cli/dist/cli.js dev examples/weather/src/server.ts --app weather\n\n: # 2. in another shell, call the printed local endpoint\nURL=http://127.0.0.1:<port>/o/local/weather/dev/mcp\ncurl -s \"$URL\" \\\n -H 'content-type: application/json' \\\n -H 'accept: application/json, text/event-stream' \\\n -H 'mcp-protocol-version: 2025-11-25' \\\n -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"weather_briefing\",\"arguments\":{\"city\":\"Paris\"}}}'\n```\n\nExample result (live data, abbreviated):\n\n```json\n{\n \"place\": \"Paris\", \"country\": \"France\",\n \"temperature_c\": 25.1, \"windspeed_kmh\": 8.3,\n \"conditions\": \"overcast\",\n \"headline\": \"Paris, France: 25°C, overcast.\",\n \"advice\": \"Comfortable conditions — no special prep needed.\"\n}\n```\n\nTry other cities (`Reykjavik`, `Singapore`, `Denver`) to see the conditions and advice change.\n\nCall `search_places` to see the **list-returning** tool — one query, many matches:\n\n```bash\ncurl -s \"$URL\" \\\n -H 'content-type: application/json' \\\n -H 'accept: application/json, text/event-stream' \\\n -H 'mcp-protocol-version: 2025-11-25' \\\n -d '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"search_places\",\"arguments\":{\"query\":\"Springfield\"}}}'\n```\n\n```json\n{\n \"places\": [\n { \"id\": \"4951788\", \"label\": \"Springfield, Massachusetts, United States\" },\n { \"id\": \"4250542\", \"label\": \"Springfield, Illinois, United States\" },\n { \"id\": \"4508722\", \"label\": \"Springfield, Ohio, United States\" }\n ]\n}\n```\n" },
107
+ { relPath: "examples/weather/README.md", content: "# Weather Briefing\n\nTwo declarative tools that show the runtime's breadth working together, with **no auth and no API\nkeys**. The `weather_briefing` tool takes a city name and runs a **three-step flow**:\n\nCapability slots: HTTP connector authoring, ordered fulfilment flows, query/response mapping,\n**list-returning connector output** (a connector that returns a live, variable-length array), and\nsandboxed compute, including an explicit least-privilege per-operation response-size bound.\n\nFor a different API with an OpenAPI document, start with `noodle import openapi <file>` in a separate\ndirectory. It preserves supported typed JSON bodies and scalar parameters; unsupported input encodings\nstop import instead of dropping fields. Its offline test establishes the contract, not live behavior; follow the\n[connector guide](https://docs.noodleseed.dev/docs/guides/connectors) before replacing this curated flow.\n\n1. **`geo.search`** → geocode the city to coordinates (Open-Meteo Geocoding API)\n2. **`forecast.current`** → fetch current weather for those coordinates (Open-Meteo Forecast API)\n3. **`brief.summarize`** → derive a human-readable briefing in a **WASM/QuickJS compute sandbox**\n\nThe second tool, `search_places`, shows a connector returning a **live, variable-length list**: it binds\nthe whole Open-Meteo geocoding `results` array with `${response.results}`, then narrows each match to\n`{ id, label }` in a compute connector — the \"search → a list of options the model can pick from\"\npattern. Narrowing lives in compute because a `${...}` response mapping cannot iterate an array and a\ntool's Zod output does not strip fields at runtime.\n\nIt exercises, in one TypeScript-authored app:\n\n- **Server-level branding** with semantic tokens carried through the runtime artifact for any generated\n app surface.\n- **Ordered flow execution** with outputs threaded between steps (`${steps.geo.latitude}` → next step).\n- **Two HTTP connectors on two different hosts**, each with its own egress allowlist.\n- **Query parameters** (`query: [...]`) and a constant query baked into the path (`?current_weather=true`).\n- **Deep response mapping** with the `${...}` language — single-element indexing\n (`${response.results[0].latitude}`, `${response.current_weather.temperature}`) **and** whole-array\n binding (`${response.results}` returns the entire list verbatim).\n- **A list-returning connector + compute narrowing** — `geo.search_list` binds the whole `results`\n array; `places.narrow` reduces each element to `{ id, label }` and normalizes the no-results case\n to `[]`.\n- **A per-operation transport bound** — `search_list` sets\n `limits: { maxResponseBytes: 256 * 1024 }`, tightening this known-small endpoint below the 1 MiB default.\n The authoring ceiling is 6 MiB, but grant only the bytes representative evidence proves this operation\n needs.\n- **Sandboxed compute** (no network/fs/env/clock) turning raw numbers into conditions + advice.\n- **Typed input/output schemas** emitted as JSON Schema 2020-12.\n\n## APIs that require form-urlencoded search bodies\n\nThe live Open-Meteo calls above are GET requests. For APIs whose search endpoint is a POST expecting\n`application/x-www-form-urlencoded`, keep authoring a request object and select the encoding explicitly:\n\n```ts\nsearch_quotes: {\n type: 'read',\n method: 'POST',\n path: '/quotes/search',\n requestEncoding: 'form-urlencoded',\n input: z.object({\n fromAirportId: z.string(),\n categories: z.array(z.string()),\n }),\n request: {\n 'from airport id': '${args.fromAirportId}',\n 'aircraft[categories]': '${args.categories}',\n },\n // output and response mapping omitted\n},\n```\n\nNoodle builds a `URLSearchParams` body: spaces and punctuation in field names are encoded normally, while\neach array or nested object is JSON-stringified into its individual form field. Do not pre-encode the body\nor set `Content-Type` manually; the connector owns both.\n\n## Run it locally\n\nFrom the repo root, with the workspace built (`pnpm build`):\n\n```bash\n: # 1. boot the local loopback dev server\nnode packages/cli/dist/cli.js dev examples/weather/src/server.ts --app weather\n\n: # 2. in another shell, call the printed local endpoint\nURL=http://127.0.0.1:<port>/o/local/weather/dev/mcp\ncurl -s \"$URL\" \\\n -H 'content-type: application/json' \\\n -H 'accept: application/json, text/event-stream' \\\n -H 'mcp-protocol-version: 2025-11-25' \\\n -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"weather_briefing\",\"arguments\":{\"city\":\"Paris\"}}}'\n```\n\nExample result (live data, abbreviated):\n\n```json\n{\n \"place\": \"Paris\", \"country\": \"France\",\n \"temperature_c\": 25.1, \"windspeed_kmh\": 8.3,\n \"conditions\": \"overcast\",\n \"headline\": \"Paris, France: 25°C, overcast.\",\n \"advice\": \"Comfortable conditions — no special prep needed.\"\n}\n```\n\nTry other cities (`Reykjavik`, `Singapore`, `Denver`) to see the conditions and advice change.\n\nCall `search_places` to see the **list-returning** tool — one query, many matches:\n\n```bash\ncurl -s \"$URL\" \\\n -H 'content-type: application/json' \\\n -H 'accept: application/json, text/event-stream' \\\n -H 'mcp-protocol-version: 2025-11-25' \\\n -d '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"search_places\",\"arguments\":{\"query\":\"Springfield\"}}}'\n```\n\n```json\n{\n \"places\": [\n { \"id\": \"4951788\", \"label\": \"Springfield, Massachusetts, United States\" },\n { \"id\": \"4250542\", \"label\": \"Springfield, Illinois, United States\" },\n { \"id\": \"4508722\", \"label\": \"Springfield, Ohio, United States\" }\n ]\n}\n```\n" },
108
108
  { relPath: "examples/weather/noodle.json", content: "{\n \"entrypoint\": \"src/server.ts\",\n \"name\": \"weather\"\n}\n" },
109
109
  { relPath: "examples/weather/package.json", content: "{\n \"name\": \"weather\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"test\": \"vitest run\",\n \"validate\": \"noodle validate\",\n \"dev\": \"noodle dev\",\n \"deploy\": \"noodle deploy\"\n },\n \"devDependencies\": {\n \"@noodleseed/one\": \"latest\",\n \"vitest\": \"latest\"\n }\n}\n" },
110
110
  { relPath: "examples/weather/src/server.ts", content: "import { connector, server, tool, z } from '@noodleseed/one';\n\n// The same Weather Briefing server, authored in TypeScript with the Noodle authoring SDK.\n//\n// The SDK owns the *manifest*: the tool, its Zod-typed input/output schemas, and the flow — which you\n// write as ordinary code in `fulfil` and the SDK records symbolically into ordered steps.\n//\n// HTTP and compute connectors are authored here too, so the public developer entrypoint is one\n// self-contained server.ts. The SDK still compiles this to internal manifest/catalog data for the runtime.\n\nconst geocoding = connector('open_meteo_geocoding')\n .version('1.0.0')\n .http({\n baseUrl: 'https://geocoding-api.open-meteo.com',\n allowedOrigins: ['https://geocoding-api.open-meteo.com'],\n operations: {\n search: {\n type: 'read',\n method: 'GET',\n path: '/v1/search',\n query: ['name'],\n input: z.object({ name: z.string() }),\n output: z.object({\n latitude: z.number(),\n longitude: z.number(),\n place: z.string().optional(),\n country: z.string().optional(),\n }),\n response: {\n latitude: '${response.results[0].latitude}',\n longitude: '${response.results[0].longitude}',\n place: '${response.results[0].name}',\n country: '${response.results[0].country}',\n },\n },\n // A LIST-returning read. `${response.results}` binds the WHOLE array verbatim — a\n // variable-length list of place objects — with no pagination (Open-Meteo returns every match in\n // one page). Contrast the `search` op above, which indexes a single element (`results[0]`). To\n // reduce each element to a few fields, narrow it in the `geo_places` compute connector below: a\n // response mapping cannot iterate an array, and a tool's Zod output does not strip fields at\n // runtime.\n search_list: {\n type: 'read',\n method: 'GET',\n path: '/v1/search',\n query: ['name', 'count'],\n // This endpoint is intentionally small; tighten its allowance below the 1 MiB default.\n limits: { maxResponseBytes: 256 * 1024 },\n input: z.object({ name: z.string(), count: z.number().optional() }),\n output: z.object({ results: z.array(z.unknown()).optional() }),\n response: {\n results: '${response.results}',\n },\n },\n },\n });\n\nconst forecast = connector('open_meteo_forecast')\n .version('1.0.0')\n .http({\n baseUrl: 'https://api.open-meteo.com',\n allowedOrigins: ['https://api.open-meteo.com'],\n operations: {\n current: {\n type: 'read',\n method: 'GET',\n path: '/v1/forecast?current_weather=true',\n query: ['latitude', 'longitude'],\n input: z.object({ latitude: z.number(), longitude: z.number() }),\n output: z.object({\n temperature: z.number().optional(),\n windspeed: z.number().optional(),\n weathercode: z.number().optional(),\n }),\n response: {\n temperature: '${response.current_weather.temperature}',\n windspeed: '${response.current_weather.windspeed}',\n weathercode: '${response.current_weather.weathercode}',\n },\n },\n },\n });\n\nconst brief = connector('weather_brief')\n .version('1.0.0')\n .compute('summarize', {\n type: 'read',\n input: z.object({\n place: z.string(),\n country: z.string().optional(),\n temperature: z.number(),\n windspeed: z.number(),\n weathercode: z.number(),\n }),\n output: z.object({\n conditions: z.string(),\n headline: z.string(),\n advice: z.string(),\n }),\n // A real function — type-checked here, serialized to source and run in the sandbox. It must be\n // self-contained: no imports, no closure over outer variables, synchronous.\n run: (input) => {\n const codes: Record<number, string> = {\n 0: 'clear sky',\n 1: 'mainly clear',\n 2: 'partly cloudy',\n 3: 'overcast',\n 45: 'fog',\n 48: 'depositing rime fog',\n 51: 'light drizzle',\n 53: 'moderate drizzle',\n 55: 'dense drizzle',\n 61: 'slight rain',\n 63: 'moderate rain',\n 65: 'heavy rain',\n 71: 'slight snow',\n 73: 'moderate snow',\n 75: 'heavy snow',\n 77: 'snow grains',\n 80: 'slight rain showers',\n 81: 'moderate rain showers',\n 82: 'violent rain showers',\n 85: 'slight snow showers',\n 86: 'heavy snow showers',\n 95: 'thunderstorm',\n 96: 'thunderstorm with hail',\n 99: 'thunderstorm with heavy hail',\n };\n const code = Number(input.weathercode);\n const conditions = codes[code] || 'unknown conditions';\n const temp = Math.round(Number(input.temperature));\n const wind = Math.round(Number(input.windspeed));\n const where = input.country ? `${input.place}, ${input.country}` : input.place;\n const headline = `${where}: ${temp}°C, ${conditions}.`;\n const tips: string[] = [];\n if (temp <= 0) tips.push(\"bundle up, it's freezing\");\n else if (temp <= 10) tips.push('wear a warm coat');\n else if (temp >= 28) tips.push(\"stay hydrated, it's hot\");\n if (code >= 95) tips.push('thunderstorms expected — seek shelter');\n else if (code >= 71 && code <= 86 && code !== 80 && code !== 81 && code !== 82)\n tips.push('snow — dress warm and tread carefully');\n else if (code >= 51 && code <= 82) tips.push('bring an umbrella');\n if (wind >= 30) tips.push('expect strong winds');\n const advice = tips.length\n ? `${tips.join('; ')}.`\n : 'Comfortable conditions — no special prep needed.';\n return { conditions, headline, advice };\n },\n });\n\n// Narrowing a live list to `{ id, label }` summaries is the ONE reshape a response mapping cannot do\n// (the `${...}` language has no per-item iteration) and a tool's Zod output does not enforce at runtime\n// — so it happens here, in a sandboxed compute connector (a connector is HTTP or compute, not both).\n// This also normalizes the no-results case (Open-Meteo omits `results` when nothing matches) to `[]`.\nconst placeNarrow = connector('geo_places')\n .version('1.0.0')\n .compute('narrow', {\n type: 'read',\n input: z.object({ results: z.unknown().optional() }),\n output: z.object({ places: z.array(z.unknown()) }),\n // Self-contained: no imports, no closure over outer variables, synchronous.\n run: (input) => {\n const raw = input.results;\n const list = Array.isArray(raw) ? raw : [];\n const places = list.map((entry) => {\n const parts = [entry.name, entry.admin1, entry.country].filter(\n (part) => typeof part === 'string' && part.length > 0,\n );\n const id =\n entry.id !== undefined && entry.id !== null\n ? String(entry.id)\n : `${entry.latitude},${entry.longitude}`;\n return { id, label: parts.join(', ') };\n });\n return { places };\n },\n });\n\nexport default server(\n 'weather_briefing',\n {\n title: 'Weather Briefing',\n version: '1.0.0',\n use: { geo: geocoding, forecast, brief, places: placeNarrow },\n branding: {\n name: 'Weather Briefing',\n accent: '#0284C7',\n radius: 'md',\n density: 'comfortable',\n },\n },\n [\n tool('weather_briefing', {\n title: 'Weather briefing',\n description:\n 'Look up a city, fetch its current weather, and return a human-readable briefing. Runs a ' +\n 'three-step flow: geocode the city, fetch the forecast, then derive the briefing in a sandboxed compute step.',\n input: z.object({\n city: z.string(),\n }),\n output: z.object({\n place: z.string(),\n country: z.string(),\n temperature_c: z.number(),\n windspeed_kmh: z.number(),\n conditions: z.string(),\n headline: z.string(),\n advice: z.string(),\n }),\n fulfil: ({ input, connectors }) => {\n const located = connectors.geo.search({ name: input.city });\n const weather = connectors.forecast.current({\n latitude: located.latitude,\n longitude: located.longitude,\n });\n const briefing = connectors.brief.summarize({\n place: located.place,\n country: located.country,\n temperature: weather.temperature,\n windspeed: weather.windspeed,\n weathercode: weather.weathercode,\n });\n return {\n place: located.place,\n country: located.country,\n temperature_c: weather.temperature,\n windspeed_kmh: weather.windspeed,\n conditions: briefing.conditions,\n headline: briefing.headline,\n advice: briefing.advice,\n };\n },\n }),\n // A connector that returns a live, variable-length LIST: search a place name, get back the\n // matching locations as `{ id, label }` options the model can resolve against. The HTTP op binds\n // the whole array; the compute connector narrows each element to the two fields the model speaks\n // from. Append new tools AFTER existing ones so `tools[0]` stays stable for host harnesses.\n tool('search_places', {\n title: 'Search places',\n description:\n 'Search a place name and return the matching locations as a list of { id, label } options.',\n // Bound the list at the source: `limit` is capped in the schema and passed through to the\n // upstream `count` parameter, so the model can never pull an unbounded page into its context.\n // `noodle check` reports an unbounded array output as `tool_design_output_bounds`.\n input: z.object({\n query: z.string(),\n limit: z.number().int().min(1).max(10).default(5),\n }),\n output: z.object({\n places: z.array(z.object({ id: z.string(), label: z.string() })),\n }),\n fulfil: ({ input, connectors }) => {\n const found = connectors.geo.search_list({ name: input.query, count: input.limit });\n const narrowed = connectors.places.narrow({ results: found.results });\n return { places: narrowed.places };\n },\n }),\n ],\n);\n" },
@@ -39,7 +39,7 @@ export function renderAuthoringWorkflowReference() {
39
39
  '## Input paths',
40
40
  '',
41
41
  '1. **Website scrape** — if the user gives a URL, scrape it for surface hints (products, services, hours, contact, pricing). Stop there: the URL does not reveal CRM, booking systems, custom APIs, auth model, eligibility rules, quoting logic, or approval flows. Those live in the business systems and the owner’s head — ask.',
42
- '2. **OpenAPI import** — `noodle import openapi <file>` writes a pinned project at `src/server.ts`, shared operation/tool schemas, managed auth references, and an offline contract test. It does not install dependencies or call the backend. Follow its README, run `agent:check`, and add a reviewed sandbox-operation test; compile success is not live integration proof. Review unsupported-body/auth warnings before deployment. Modified files are preserved unless `--force` is explicit.',
42
+ '2. **OpenAPI import** — `noodle import openapi <file>` writes a pinned project at `src/server.ts`, shared operation/tool schemas, managed auth references, and an offline contract test. Supported JSON bodies become typed `input.body`, sent unchanged; unsupported request constraints/encodings stop import before writes. It does not install dependencies or call the backend. Follow its README, run `agent:check`, and add a reviewed sandbox-operation test; compile success is not live integration proof. Review auth warnings before deployment. Modified files are preserved unless `--force` is explicit.',
43
43
  '3. **Upstream MCP import** — `noodle import mcp <url> --name <slug> --output <dir>` discovers `tools/list` once, validates and freezes tool schemas into TypeScript, and writes a secret-free drift snapshot. Upstream annotations are untrusted, so generated tools remain destructive confirmed actions until an author verifies and narrows them. Use `--header-env <header>=<ENV_NAME>` for import-only auth and `--check` for classified, non-mutating drift detection. Runtime never performs discovery.',
44
44
  'Both imports use `src/server.ts` as the declared entrypoint, include an offline compile test and `.env.example`, and write files only. Install the pinned dependencies, run the generated checks, then `noodle agents setup --apply`. Never treat generated contract tests as customer authentication or business-workflow evidence.',
45
45
  '4. **User interview** — Noodle does not interview; you do. Cover custom APIs/integrations, eligibility rules, quoting/approval logic, and private schemas (SQL DDL or JSON samples for custom `connector` declarations). Ask for concrete examples and sample payloads; do not guess a schema from a URL or invent endpoints.',
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noodle-borg/agent-kit",
3
- "version": "0.95.0",
3
+ "version": "0.96.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,39 +1,69 @@
1
1
  import YAML from 'yaml';
2
+ import { importRequestBody } from './request-body.js';
3
+ import { identifier, importOperationParameters } from './request-input.js';
2
4
  import { importResponseSchema, toOutputJsonSchema, } from './response-schema.js';
3
5
  export * from './mcp/index.js';
6
+ export { identifier } from './request-input.js';
4
7
  export { importResponseSchema, MAX_RESPONSE_SCHEMA_DEPTH, MAX_RESPONSE_SCHEMA_NODES, toOutputJsonSchema, } from './response-schema.js';
5
8
  const METHODS = new Set(['get', 'post', 'put', 'patch', 'delete']);
9
+ export const MAX_OPENAPI_SOURCE_BYTES = 6 * 1024 * 1024;
6
10
  export function parseOpenApiToIr(source, options) {
7
- const doc = YAML.parse(source);
11
+ if (Buffer.byteLength(source, 'utf8') > MAX_OPENAPI_SOURCE_BYTES) {
12
+ throw new Error('import openapi: document exceeds the 6 MiB size limit; import a scoped API document');
13
+ }
14
+ let parsed;
15
+ try {
16
+ parsed = YAML.parse(source, { maxAliasCount: 100 });
17
+ }
18
+ catch {
19
+ throw new Error('import openapi: invalid JSON/YAML document; repair its syntax and retry');
20
+ }
21
+ if (!isRecord(parsed))
22
+ throw new Error('import openapi: document must be an object');
23
+ const doc = parsed;
8
24
  if (typeof doc.openapi !== 'string' || !/^3\.[01]\./.test(doc.openapi)) {
9
25
  throw new Error('import openapi: only OpenAPI 3.0 and 3.1 documents are supported');
10
26
  }
11
- const baseUrl = options.baseUrl ?? doc.servers?.find((server) => typeof server.url === 'string')?.url;
27
+ const baseUrl = options.baseUrl ??
28
+ (Array.isArray(doc.servers)
29
+ ? doc.servers.find((server) => isRecord(server) && typeof server.url === 'string')?.url
30
+ : undefined);
12
31
  if (baseUrl === undefined)
13
32
  throw new Error('import openapi: missing base URL; pass --base-url');
33
+ try {
34
+ const base = new URL(baseUrl);
35
+ if (!['https:', 'http:'].includes(base.protocol) ||
36
+ base.username ||
37
+ base.password ||
38
+ base.search ||
39
+ base.hash)
40
+ throw new Error();
41
+ }
42
+ catch {
43
+ throw new Error('import openapi: use an HTTP(S) base URL without credentials, query or fragment');
44
+ }
14
45
  const operations = [];
15
46
  const warnings = [];
16
47
  const referencedSchemes = [];
17
48
  collectSecurityRefs(doc.security, referencedSchemes);
18
- for (const [path, item] of Object.entries(doc.paths ?? {})) {
49
+ if (!isRecord(doc.paths))
50
+ throw new Error('import openapi: paths must be an object');
51
+ for (const [path, item] of Object.entries(doc.paths)) {
52
+ if (!isRecord(item) ||
53
+ !path.startsWith('/') ||
54
+ path.startsWith('//') ||
55
+ /[?#\p{Cc}]/u.test(path)) {
56
+ throw new Error('import openapi: use path-item objects with relative paths and declared query parameters');
57
+ }
19
58
  for (const [method, rawOperation] of Object.entries(item)) {
20
59
  if (!METHODS.has(method))
21
60
  continue;
61
+ if (!isRecord(rawOperation))
62
+ throw new Error('import openapi: each supported operation must be an object');
22
63
  const operation = rawOperation;
23
64
  collectSecurityRefs(operation.security, referencedSchemes);
24
65
  const name = identifier(typeof operation.operationId === 'string' ? operation.operationId : `${method}_${path}`);
25
- const parameters = (operation.parameters ?? [])
26
- .filter((param) => param.in === 'path' || param.in === 'query')
27
- .map((param) => ({
28
- name: identifier(String(param.name ?? 'param')),
29
- in: param.in,
30
- required: param.required === true,
31
- schema: parameterJsonSchema(param.schema),
32
- }));
33
- const hasRequestBody = operation.requestBody !== undefined;
34
- if (hasRequestBody) {
35
- warnings.push(`${name}: requestBody imported as opaque JSON string placeholder`);
36
- }
66
+ const { parameters, connectorPath } = importOperationParameters(path, item.parameters, operation.parameters);
37
67
  const safeName = uniqueName(manifestIdentifier(name), new Set(operations.map((op) => op.safeName)));
38
68
  const responseCtx = {
39
69
  operationName: name,
@@ -41,6 +71,13 @@ export function parseOpenApiToIr(source, options) {
41
71
  warnings,
42
72
  };
43
73
  const output = importResponseSchema(operation.responses, responseCtx);
74
+ const requestBody = importRequestBody(operation.requestBody, responseCtx);
75
+ if (requestBody !== undefined && method === 'get') {
76
+ throw new Error('import openapi: GET request bodies require an explicitly authored mapping');
77
+ }
78
+ if (requestBody !== undefined && parameters.some((param) => param.name === 'body')) {
79
+ throw new Error('import openapi: request body and parameter input-name collision; map the inputs explicitly');
80
+ }
44
81
  operations.push({
45
82
  name,
46
83
  safeName,
@@ -49,10 +86,10 @@ export function parseOpenApiToIr(source, options) {
49
86
  path,
50
87
  // OpenAPI's `{name}` path params are already the connector runtime's template form; rewriting
51
88
  // them to `${args.name}` made catalog compilation reject the path (unsupported_path_expression).
52
- connectorPath: path,
89
+ connectorPath,
53
90
  description: `Call ${method.toUpperCase()} ${path}.`,
54
91
  parameters,
55
- hasRequestBody,
92
+ ...(requestBody === undefined ? {} : { requestBody }),
56
93
  query: parameters.filter((param) => param.in === 'query').map((param) => param.name),
57
94
  ...(output !== undefined ? { output } : {}),
58
95
  });
@@ -124,7 +161,7 @@ export function renderOpenApiConnectorsYaml(ir) {
124
161
  kind: 'custom',
125
162
  http: {
126
163
  baseUrl: ir.baseUrl,
127
- allowedOrigins: [ir.baseUrl],
164
+ allowedOrigins: [new URL(ir.baseUrl).origin],
128
165
  ...(ir.auth !== undefined ? { auth: ir.auth } : {}),
129
166
  },
130
167
  operations: Object.fromEntries(ir.operations.map((operation) => [operation.safeName, connectorOperation(operation)])),
@@ -153,7 +190,7 @@ export function renderOpenApiManifestYaml(ir) {
153
190
  ...outputSchemaEntry(operation),
154
191
  fulfilment: {
155
192
  use: `api.${operation.safeName}`,
156
- args: Object.fromEntries(operation.parameters.map((param) => [param.name, `\${input.${param.name}}`])),
193
+ args: inputArguments(operation),
157
194
  },
158
195
  })),
159
196
  })}`;
@@ -220,7 +257,7 @@ export function mergeOpenApiIntoDraft(input) {
220
257
  ...outputSchemaEntry(operation),
221
258
  fulfilment: {
222
259
  use: `${connectorAlias}.${operationName}`,
223
- args: Object.fromEntries(operation.parameters.map((param) => [param.name, `\${input.${param.name}}`])),
260
+ args: inputArguments(operation),
224
261
  },
225
262
  });
226
263
  existingToolNames.add(toolName);
@@ -238,10 +275,6 @@ export function mergeOpenApiIntoDraft(input) {
238
275
  warnings,
239
276
  };
240
277
  }
241
- export function identifier(value) {
242
- const out = value.replace(/[^A-Za-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
243
- return /^[A-Za-z_]/.test(out) ? out : `op_${out || 'operation'}`;
244
- }
245
278
  export function title(value) {
246
279
  return value.replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
247
280
  }
@@ -255,17 +288,12 @@ function manifestIdentifier(value) {
255
288
  return /^[a-z0-9_]+$/.test(snake) && snake.length > 0 ? snake : 'operation';
256
289
  }
257
290
  function connectorOperation(operation) {
258
- const required = operation.parameters.filter((param) => param.required).map((p) => p.name);
259
291
  return {
260
292
  type: operation.operationType,
261
293
  method: operation.method.toUpperCase(),
262
294
  path: operation.connectorPath,
263
- input: {
264
- type: 'object',
265
- properties: Object.fromEntries(operation.parameters.map((param) => [param.name, parameterSchemaJson(param.schema)])),
266
- ...(required.length > 0 ? { required } : {}),
267
- additionalProperties: false,
268
- },
295
+ input: inputSchema(operation),
296
+ ...(operation.requestBody === undefined ? {} : { request: '${args.body}' }),
269
297
  ...(operation.query.length > 0 ? { query: [...operation.query] } : {}),
270
298
  // The same lossless `{ value: <tree> }` object the tool outputSchema advertises; an untyped
271
299
  // operation keeps an open ({}) value subtree. Response mapping wraps the JSON body as `value`.
@@ -292,26 +320,28 @@ function outputSchemaEntry(operation) {
292
320
  };
293
321
  }
294
322
  function inputSchema(operation) {
323
+ const required = operation.parameters
324
+ .filter((param) => param.required)
325
+ .map((param) => param.name);
326
+ if (operation.requestBody?.required)
327
+ required.push('body');
295
328
  return {
296
329
  type: 'object',
297
- properties: Object.fromEntries(operation.parameters.map((param) => [param.name, parameterSchemaJson(param.schema)])),
298
- required: operation.parameters.filter((param) => param.required).map((param) => param.name),
330
+ properties: {
331
+ ...Object.fromEntries(operation.parameters.map((param) => [param.name, parameterSchemaJson(param.schema)])),
332
+ ...(operation.requestBody === undefined
333
+ ? {}
334
+ : { body: toOutputJsonSchema(operation.requestBody.schema) }),
335
+ },
336
+ required,
299
337
  additionalProperties: false,
300
338
  };
301
339
  }
302
- /** Import one parameter's OpenAPI schema as a lossless JSON Schema fragment (see the type doc). */
303
- function parameterJsonSchema(schema) {
304
- if (!isRecord(schema))
305
- return { type: 'string' };
306
- const rawType = schema.type;
307
- const base = rawType === 'number' || rawType === 'integer' || rawType === 'boolean' ? rawType : 'string';
308
- const enumValues = base === 'string' ? stringEnum(schema.enum) : undefined;
309
- const format = typeof schema.format === 'string' ? schema.format : undefined;
310
- return {
311
- type: schema.nullable === true ? [base, 'null'] : base,
312
- ...(enumValues !== undefined ? { enum: enumValues } : {}),
313
- ...(format !== undefined ? { format } : {}),
314
- };
340
+ function inputArguments(operation) {
341
+ return Object.fromEntries([
342
+ ...operation.parameters.map((param) => [param.name, `\${input.${param.name}}`]),
343
+ ...(operation.requestBody === undefined ? [] : [['body', '${input.body}']]),
344
+ ]);
315
345
  }
316
346
  /** A fresh plain-JSON copy of a parameter schema, safe to embed in YAML documents. */
317
347
  function parameterSchemaJson(schema) {
@@ -321,13 +351,6 @@ function parameterSchemaJson(schema) {
321
351
  ...(schema.format !== undefined ? { format: schema.format } : {}),
322
352
  };
323
353
  }
324
- function stringEnum(value) {
325
- if (!Array.isArray(value) || value.length === 0)
326
- return undefined;
327
- if (!value.every((entry) => typeof entry === 'string'))
328
- return undefined;
329
- return value;
330
- }
331
354
  function parseYamlObject(source, label) {
332
355
  const parsed = YAML.parse(source);
333
356
  if (!isRecord(parsed))
@@ -0,0 +1,125 @@
1
+ import { importSchemaTree, MAX_RESPONSE_SCHEMA_DEPTH, MAX_RESPONSE_SCHEMA_NODES, } from './response-schema.js';
2
+ /** Request inputs must not silently lose constraints or become an untyped body placeholder. */
3
+ export function importRequestBody(value, ctx) {
4
+ if (value === undefined)
5
+ return undefined;
6
+ if (!isRecord(value) || value.$ref !== undefined) {
7
+ throw new Error('import openapi: inline the request body declaration before import');
8
+ }
9
+ if (value.required !== undefined && typeof value.required !== 'boolean') {
10
+ throw new Error('import openapi: request body required must be boolean');
11
+ }
12
+ const content = isRecord(value.content) ? value.content : undefined;
13
+ const media = content?.['application/json'];
14
+ const raw = isRecord(media) ? media.schema : undefined;
15
+ if (raw === undefined)
16
+ throw new Error('import openapi: request body must declare an application/json schema; map other encodings explicitly');
17
+ assertRequestSchema(raw, ctx);
18
+ const warningCount = ctx.warnings.length;
19
+ const schema = importSchemaTree(raw, ctx, true);
20
+ if (schema.kind === 'unknown' || ctx.warnings.length !== warningCount) {
21
+ throw new Error('import openapi: request body schema cannot be imported faithfully; simplify it or author its mapping explicitly');
22
+ }
23
+ return { required: value.required === true, schema };
24
+ }
25
+ function assertRequestSchema(raw, ctx) {
26
+ const allowed = new Set([
27
+ 'type',
28
+ 'nullable',
29
+ 'properties',
30
+ 'required',
31
+ 'items',
32
+ 'enum',
33
+ 'additionalProperties',
34
+ '$ref',
35
+ 'title',
36
+ 'description',
37
+ 'example',
38
+ 'examples',
39
+ 'deprecated',
40
+ ]);
41
+ let nodes = 0;
42
+ const visit = (value, depth, refs) => {
43
+ nodes++;
44
+ if (!isRecord(value) ||
45
+ depth > MAX_RESPONSE_SCHEMA_DEPTH ||
46
+ nodes > MAX_RESPONSE_SCHEMA_NODES) {
47
+ throw new Error('import openapi: request body schema exceeds supported structure/depth/size bounds');
48
+ }
49
+ if (Object.keys(value).some((key) => !allowed.has(key))) {
50
+ throw new Error('import openapi: request body contains an unsupported schema keyword; author its validated mapping explicitly');
51
+ }
52
+ if (value.$ref !== undefined) {
53
+ if (Object.keys(value).some((key) => !['$ref', 'title', 'description', 'example', 'examples', 'deprecated'].includes(key))) {
54
+ throw new Error('import openapi: request body reference siblings cannot be imported faithfully; inline the schema');
55
+ }
56
+ if (typeof value.$ref !== 'string' ||
57
+ !value.$ref.startsWith('#/components/schemas/') ||
58
+ refs.has(value.$ref)) {
59
+ throw new Error('import openapi: request body has an external, circular or unsupported reference; bundle it into an inline schema');
60
+ }
61
+ const name = value.$ref
62
+ .slice('#/components/schemas/'.length)
63
+ .replace(/~1/g, '/')
64
+ .replace(/~0/g, '~');
65
+ visit(ctx.componentSchemas[name], depth + 1, new Set([...refs, value.$ref]));
66
+ return;
67
+ }
68
+ const types = Array.isArray(value.type) ? value.type : [value.type];
69
+ const concrete = types.filter((type) => type !== 'null');
70
+ const type = concrete[0];
71
+ if (concrete.length !== 1 ||
72
+ typeof type !== 'string' ||
73
+ !['string', 'number', 'integer', 'boolean', 'object', 'array'].includes(type) ||
74
+ types.length > 2) {
75
+ throw new Error('import openapi: request body requires a supported explicit type at every schema node');
76
+ }
77
+ if (value.nullable !== undefined && typeof value.nullable !== 'boolean') {
78
+ throw new Error('import openapi: request body nullable must be boolean');
79
+ }
80
+ if ((value.properties !== undefined ||
81
+ value.required !== undefined ||
82
+ value.additionalProperties !== undefined) &&
83
+ type !== 'object') {
84
+ throw new Error('import openapi: request body object keywords require an object type');
85
+ }
86
+ if ((type === 'array' && value.items === undefined) ||
87
+ (value.items !== undefined && type !== 'array')) {
88
+ throw new Error('import openapi: request body arrays require one typed items schema');
89
+ }
90
+ if (value.additionalProperties !== undefined &&
91
+ typeof value.additionalProperties !== 'boolean') {
92
+ throw new Error('import openapi: request body additionalProperties must be boolean');
93
+ }
94
+ if (value.enum !== undefined &&
95
+ (!Array.isArray(value.enum) ||
96
+ value.enum.length === 0 ||
97
+ !value.enum.every((item) => typeof item === 'string'))) {
98
+ throw new Error('import openapi: request body supports only nonempty string enums');
99
+ }
100
+ if (value.enum !== undefined &&
101
+ (type !== 'string' || types.includes('null') || value.nullable === true)) {
102
+ throw new Error('import openapi: request body enum/nullable combination requires an explicit mapping');
103
+ }
104
+ if (value.required !== undefined &&
105
+ (!Array.isArray(value.required) ||
106
+ !value.required.every((name) => typeof name === 'string' &&
107
+ isRecord(value.properties) &&
108
+ Object.hasOwn(value.properties, name)))) {
109
+ throw new Error('import openapi: request body required fields must have declared properties');
110
+ }
111
+ if (value.properties !== undefined) {
112
+ if (!isRecord(value.properties))
113
+ throw new Error('import openapi: request body properties must be an object');
114
+ for (const child of Object.values(value.properties))
115
+ visit(child, depth + 1, refs);
116
+ }
117
+ if (value.items !== undefined)
118
+ visit(value.items, depth + 1, refs);
119
+ };
120
+ visit(raw, 0, new Set());
121
+ }
122
+ function isRecord(value) {
123
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
124
+ }
125
+ //# sourceMappingURL=request-body.js.map
@@ -0,0 +1,139 @@
1
+ const FIELD = /^[A-Za-z_][A-Za-z0-9_]*$/;
2
+ const RESERVED_FIELDS = new Set([
3
+ 'then',
4
+ 'equals',
5
+ 'at',
6
+ 'optional',
7
+ 'toExpression',
8
+ 'toString',
9
+ '__proto__',
10
+ 'constructor',
11
+ 'prototype',
12
+ ]);
13
+ export function identifier(value) {
14
+ const out = value.replace(/[^A-Za-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
15
+ return /^[A-Za-z_]/.test(out) ? out : `op_${out || 'operation'}`;
16
+ }
17
+ /** Keep wire query names intact; path placeholder names may be normalized with their arguments. */
18
+ export function importOperationParameters(path, inherited, own) {
19
+ const selected = new Map();
20
+ for (const list of [inherited, own]) {
21
+ if (list === undefined)
22
+ continue;
23
+ if (!Array.isArray(list))
24
+ throw new Error('import openapi: parameters must be an array');
25
+ for (const value of list) {
26
+ if (!isRecord(value) ||
27
+ typeof value.name !== 'string' ||
28
+ value.name.length === 0 ||
29
+ value.name.length > 128 ||
30
+ /\p{Cc}/u.test(value.name)) {
31
+ throw new Error('import openapi: use inline parameters with bounded, non-control names');
32
+ }
33
+ if (value.in !== 'path' && value.in !== 'query') {
34
+ throw new Error('import openapi: only path/query parameters are supported; map other locations explicitly in TypeScript');
35
+ }
36
+ if (value.style !== undefined && value.style !== (value.in === 'path' ? 'simple' : 'form')) {
37
+ throw new Error('import openapi: unsupported parameter serialization style; map it explicitly in TypeScript');
38
+ }
39
+ if (value.allowReserved === true ||
40
+ value.allowEmptyValue === true ||
41
+ value.content !== undefined) {
42
+ throw new Error('import openapi: unsupported parameter wire encoding; map it explicitly in TypeScript');
43
+ }
44
+ selected.set(`${value.in}:${value.name}`, value);
45
+ }
46
+ }
47
+ if (selected.size > 200)
48
+ throw new Error('import openapi: operation exceeds 200 parameters');
49
+ const names = new Set();
50
+ const pathNames = new Map();
51
+ const parameters = [...selected.values()].map((value) => {
52
+ const rawName = String(value.name);
53
+ let name = value.in === 'path' ? identifier(rawName) : rawName;
54
+ if (value.in === 'path' && RESERVED_FIELDS.has(name))
55
+ name = `param_${name}`;
56
+ if (!FIELD.test(name) || RESERVED_FIELDS.has(name)) {
57
+ throw new Error('import openapi: query parameter name cannot be addressed by the authoring expression contract; map this API through an application handler');
58
+ }
59
+ if (names.has(name))
60
+ throw new Error('import openapi: parameter input-name collision; map the inputs explicitly');
61
+ names.add(name);
62
+ if (value.in === 'path')
63
+ pathNames.set(rawName, name);
64
+ return {
65
+ name,
66
+ in: value.in === 'path' ? 'path' : 'query',
67
+ required: value.in === 'path' || value.required === true,
68
+ schema: parameterJsonSchema(value.schema),
69
+ };
70
+ });
71
+ const placeholders = [...path.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]);
72
+ if (placeholders.some((name) => name === undefined || !pathNames.has(name)) ||
73
+ [...pathNames.keys()].some((name) => !placeholders.includes(name))) {
74
+ throw new Error('import openapi: every path placeholder must match a declared path parameter');
75
+ }
76
+ return {
77
+ parameters,
78
+ connectorPath: path.replace(/\{([^{}]+)\}/g, (_match, name) => `{${pathNames.get(name)}}`),
79
+ };
80
+ }
81
+ function parameterJsonSchema(schema) {
82
+ if (schema === undefined)
83
+ return { type: 'string' };
84
+ if (!isRecord(schema))
85
+ throw new Error('import openapi: parameter schema must be an object');
86
+ if (schema.$ref !== undefined)
87
+ throw new Error('import openapi: inline parameter schemas before import; references are not fetched');
88
+ const allowed = new Set([
89
+ 'type',
90
+ 'nullable',
91
+ 'enum',
92
+ 'format',
93
+ 'title',
94
+ 'description',
95
+ 'example',
96
+ 'examples',
97
+ 'deprecated',
98
+ ]);
99
+ if (Object.keys(schema).some((key) => !allowed.has(key))) {
100
+ throw new Error('import openapi: unsupported parameter schema constraint; author its validated mapping explicitly');
101
+ }
102
+ const types = Array.isArray(schema.type)
103
+ ? schema.type
104
+ : [schema.type ?? 'string'];
105
+ const concrete = types.filter((type) => type !== 'null');
106
+ const base = concrete[0];
107
+ if (concrete.length !== 1 ||
108
+ types.length > 2 ||
109
+ typeof base !== 'string' ||
110
+ !['string', 'number', 'integer', 'boolean'].includes(base)) {
111
+ throw new Error('import openapi: unsupported parameter schema; use a scalar or an explicitly authored mapping');
112
+ }
113
+ const enumValues = base === 'string' ? stringEnum(schema.enum) : undefined;
114
+ if (schema.enum !== undefined &&
115
+ (enumValues === undefined || types.includes('null') || schema.nullable === true)) {
116
+ throw new Error('import openapi: unsupported parameter enum/nullable combination; author an explicit mapping');
117
+ }
118
+ const format = typeof schema.format === 'string' ? schema.format : undefined;
119
+ if ((schema.nullable !== undefined && typeof schema.nullable !== 'boolean') ||
120
+ (schema.format !== undefined && format === undefined)) {
121
+ throw new Error('import openapi: invalid parameter nullable or format declaration');
122
+ }
123
+ return {
124
+ type: schema.nullable === true || types.includes('null') ? [base, 'null'] : base,
125
+ ...(enumValues !== undefined ? { enum: enumValues } : {}),
126
+ ...(format !== undefined ? { format } : {}),
127
+ };
128
+ }
129
+ function stringEnum(value) {
130
+ if (!Array.isArray(value) || value.length === 0)
131
+ return undefined;
132
+ if (!value.every((entry) => typeof entry === 'string'))
133
+ return undefined;
134
+ return value;
135
+ }
136
+ function isRecord(value) {
137
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
138
+ }
139
+ //# sourceMappingURL=request-input.js.map
@@ -8,6 +8,10 @@ export const MAX_RESPONSE_SCHEMA_DEPTH = 8;
8
8
  export const MAX_RESPONSE_SCHEMA_NODES = 200;
9
9
  const UNKNOWN = { kind: 'unknown' };
10
10
  const UNSUPPORTED_COMPOSITIONS = ['oneOf', 'anyOf', 'allOf', 'not'];
11
+ /** Shared bounded conversion; request validation is owned by request-body.ts. */
12
+ export function importSchemaTree(raw, ctx, request = false) {
13
+ return convertSchema(raw, ctx, { nodes: 0, boundWarned: false, request }, 0, new Set());
14
+ }
11
15
  /**
12
16
  * Picks the best 2xx `application/json` schema from an operation's `responses` block and
13
17
  * converts it. Returns `undefined` (with warnings pushed to the context) when the operation
@@ -41,14 +45,17 @@ export function toOutputJsonSchema(schema) {
41
45
  case 'string':
42
46
  return withNullable({ type: 'string', ...(schema.enum !== undefined ? { enum: [...schema.enum] } : {}) }, schema.nullable);
43
47
  case 'number':
44
- return withNullable({ type: 'number' }, schema.nullable);
48
+ return withNullable({ type: schema.integer === true ? 'integer' : 'number' }, schema.nullable);
45
49
  case 'boolean':
46
50
  return withNullable({ type: 'boolean' }, schema.nullable);
47
51
  case 'array':
48
52
  return withNullable({ type: 'array', items: toOutputJsonSchema(schema.items) }, schema.nullable);
49
53
  case 'object': {
54
+ const extra = schema.additionalProperties === undefined
55
+ ? {}
56
+ : { additionalProperties: schema.additionalProperties };
50
57
  if (schema.properties.length === 0)
51
- return withNullable({ type: 'object' }, schema.nullable);
58
+ return withNullable({ type: 'object', ...extra }, schema.nullable);
52
59
  const required = schema.properties
53
60
  .filter((property) => property.required)
54
61
  .map((property) => property.name);
@@ -59,6 +66,7 @@ export function toOutputJsonSchema(schema) {
59
66
  toOutputJsonSchema(property.schema),
60
67
  ])),
61
68
  ...(required.length > 0 ? { required } : {}),
69
+ ...extra,
62
70
  }, schema.nullable);
63
71
  }
64
72
  case 'unknown':
@@ -101,7 +109,11 @@ function convertSchema(raw, ctx, state, depth, visitedRefs) {
101
109
  }
102
110
  case 'number':
103
111
  case 'integer':
104
- return { kind: 'number', ...nullableSpread };
112
+ return {
113
+ kind: 'number',
114
+ ...nullableSpread,
115
+ ...(state.request && type === 'integer' ? { integer: true } : {}),
116
+ };
105
117
  case 'boolean':
106
118
  return { kind: 'boolean', ...nullableSpread };
107
119
  case 'array':
@@ -120,6 +132,7 @@ function convertSchema(raw, ctx, state, depth, visitedRefs) {
120
132
  return {
121
133
  kind: 'object',
122
134
  ...nullableSpread,
135
+ ...(state.request ? { additionalProperties: raw.additionalProperties !== false } : {}),
123
136
  properties: Object.entries(properties).map(([name, propertySchema]) => ({
124
137
  name,
125
138
  required: required.has(name),
@@ -143,7 +156,7 @@ function convertRef(ref, ctx, state, depth, visitedRefs) {
143
156
  ctx.warnings.push(`${ctx.operationName}: response schema $ref "${ref}" is not a component schema reference; that part is left untyped`);
144
157
  return UNKNOWN;
145
158
  }
146
- const name = ref.slice(prefix.length);
159
+ const name = ref.slice(prefix.length).replace(/~1/g, '/').replace(/~0/g, '~');
147
160
  if (visitedRefs.has(name)) {
148
161
  ctx.warnings.push(`${ctx.operationName}: response schema $ref "${ref}" is circular; the repeated part is left untyped`);
149
162
  return UNKNOWN;
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@modelcontextprotocol/sdk": "^1.29.0",
41
41
  "@noodle-borg/admission-limits": "0.0.0",
42
- "@noodle-borg/agent-kit": "0.95.0",
42
+ "@noodle-borg/agent-kit": "0.96.0",
43
43
  "@noodle-borg/app-package": "0.0.0",
44
44
  "@noodle-borg/assistant-gateway": "0.0.0",
45
45
  "@noodle-borg/auth": "0.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noodleseed/one",
3
- "version": "0.158.0",
3
+ "version": "0.159.0",
4
4
  "private": false,
5
5
  "description": "Noodle CLI by Noodle Seed — author, run, and deploy declarative MCP servers. Embedding the assistant in your own web app is @noodleseed/assistant.",
6
6
  "license": "Apache-2.0",
@@ -235,7 +235,7 @@
235
235
  "@modelcontextprotocol/client": "2.0.0",
236
236
  "@modelcontextprotocol/server": "2.0.0",
237
237
  "@noodle-borg/admission-limits": "0.0.0",
238
- "@noodle-borg/agent-kit": "0.95.0",
238
+ "@noodle-borg/agent-kit": "0.96.0",
239
239
  "@noodle-borg/app-audit": "0.0.0",
240
240
  "@noodle-borg/app-package": "0.0.0",
241
241
  "@noodle-borg/assistant-gateway": "0.0.0",