Connectors
Reach real HTTP APIs and run sandboxed logic. Connectors are declared as data with brokered credentials.
A connector is how a Noodle Seed server reaches a backing system: a SaaS API, a first-party service, or your own API. Connectors are declared as data, and their credentials are resolved by the platform's credential broker, never hard-coded.
Build one with the connector builder, then bind it on the server's use map so tools can call it.
HTTP connectors
connector(id).version(v).http({...}) describes a real HTTP API: a baseUrl, an auth block, and an
operations map. Requests and responses map with ${...} expressions.
import { annotations, server, tool, connector, secret, z } from '@noodleseed/one';
const weatherInput = z.object({ city: z.string() });
const weatherOutput = z.object({ temp_c: z.number() });
const weather = connector('weather')
.version('1.0.0')
.http({
baseUrl: 'https://api.example.com',
allowedOrigins: ['https://api.example.com'],
auth: { kind: 'apiKey', header: 'X-Api-Key', secret: secret('WEATHER_API_KEY') },
operations: {
current: {
type: 'read',
method: 'GET',
path: '/v1/weather',
query: ['city'],
input: weatherInput,
output: weatherOutput,
response: { temp_c: '${response.temperature}' },
},
},
});
export default server('weather', { title: 'Weather', version: '1.0.0', use: { weather } }, [
tool('current_weather', {
description: 'Get the current temperature for a city.',
annotations: annotations.readOnly({ openWorld: true }),
input: weatherInput,
output: weatherOutput,
fulfil: ({ input, connectors }) => ({
temp_c: connectors.weather.current({ city: input.city }).temp_c,
}),
}),
]);Response-size limits
Every HTTP operation defaults to a 1 MiB decoded-response limit. Prefer a narrower endpoint, pagination, or a smaller upstream result before increasing it. When a representative response legitimately needs more, grant only that operation the required bytes (maximum 6 MiB; this example grants 3 MiB):
search: {
type: 'read',
method: 'GET',
path: '/v1/search',
limits: { maxResponseBytes: 3 * 1024 * 1024 },
// input, output, and response mapping omitted
}The bound is inclusive and counts decoded streamed bytes, including compressed responses. Response mapping
runs after the body is read, so mapping fewer fields does not reduce transport buffering. An overflow stays
a connector_error with the safe structured reason response_too_large; response bodies, headers, URLs,
and credentials are never included.
Credentials
Reference secrets and non-secret config by name; the broker resolves them per tenant at call time.
secret("NAME")for credentials (API keys, tokens). Set withnoodle secrets set.variable("NAME")for non-secret config (a base URL, a tenant id). Set withnoodle variables set.
Supported auth kinds today are bearer, apiKey, and clientCredentials
(Shipped) — the latter defaults to the RFC-6749
client-credentials grant, with profile: "custom" for non-standard partner token endpoints. For per-user
downstream access, use the supported delegated credential path described in
customer authentication, not a copied inbound bearer token.
Hosts are allowlisted
HTTP connectors are SSRF-guarded: set allowedOrigins to the exact hosts the connector may reach.
Requests to other hosts are rejected.
Expressions
An operation maps its request and response declaratively with ${...} expressions instead of imperative
code. The common scopes:
| Expression | Resolves to |
|---|---|
${args.x} | A field of the HTTP connector operation input. |
${response} | The raw HTTP response body (use ${response.field} for a field). |
${env.NAME} | A managed variable (variable("NAME")). |
operations: {
get_order: {
type: 'read',
method: 'GET',
path: '/orders/${args.id}', // interpolate a validated operation argument
input: z.object({ id: z.string() }),
output: z.object({ total: z.number() }),
response: { total: '${response.amount}' }, // map response body to output
},
}Managed config (secret("NAME") / variable("NAME")) is set with noodle secrets set /
noodle variables set and resolved per tenant by the broker at call time. Tool fulfilment inputs and
authenticated user context are separate from HTTP operation arguments; declare downstream credential
exchange explicitly and keep credentials out of request mappings and tool arguments.
Upstream MCP connectors
ShippedAdopt an existing remote Streamable HTTP MCP server with an explicit, reviewable snapshot:
noodle import mcp https://store.example/api/mcp \
--name store \
--output store-assistantThe command runs tools/list during import, validates and freezes each selected upstream tool name and
schema into generated TypeScript, and stores a secret-free drift snapshot. Runtime calls only those
declared operations. It never discovers or mirrors a changing live surface.
Both MCP and OpenAPI imports write a complete files-only project: src/server.ts, a pinned toolchain,
.env.example names and an offline contract test. Inside the generated directory, run:
npm install --ignore-scripts
npm run agent:check
npm exec -- noodle agents setup --applyThese checks compile the imported contract without calling the backend. Add a reviewed sandbox-operation
test before claiming live integration works. Review authentication and response-schema warnings.
OpenAPI imports share each operation's input/output schemas with its tool and
retain supported API-key/bearer auth as managed secret references. Set values out of band, never in source
or agent context. Imports refuse modified files unless you explicitly review and request --force; choose
a new output directory to preserve an earlier import unchanged.
OpenAPI JSON request bodies are typed under input.body and sent unchanged. Required fields, optional
bodies, nested objects/arrays, integers and nullable scalar types survive import. Path-level parameters
are inherited; query names are not silently renamed. Unsupported request encodings or constraints stop
import before writes—author those mappings explicitly using this guide. Keep input documents under 6 MiB;
external schema references are never fetched. This is a maintained starting point, not support for every
OpenAPI serialization feature.
Upstream MCP annotations are hints, not a trust boundary. Every generated tool therefore starts as a
destructive confirmed action. Verify the actual upstream behavior, keep only the operations you intend to
publish, and deliberately change proven reads to read-only annotations. An upstream readOnlyHint never
silently removes Noodle's confirmation boundary.
For import-only authentication, read one header from an environment variable. Its value is kept in memory and never written:
export STORE_IMPORT_TOKEN=…
noodle import mcp https://store.example/api/mcp \
--name store \
--header-env Authorization=STORE_IMPORT_TOKENUse --check in CI to compare the live tool surface with the saved snapshot without changing source:
noodle import mcp https://store.example/api/mcp \
--output store-assistant \
--checkThe review classifies additions as additive, removals or contract changes as breaking, and description-only
changes as metadata-only. Every class still requires explicit author review; --check never accepts drift.
The generated .mcp({...}) connector is a backing contract. Publish ordinary intent-shaped tool(...)
capabilities with stable names, narrower inputs and outputs, annotations, confirmation, and optional React
views. One public tool can combine several MCP, HTTP, and compute operations. A Noodle view can add a widget
to an upstream tool that supplied no UI.
Curated, not transparent
Noodle does not forward upstream resources, prompts, annotations, _meta, widgets, or CSP. It does not
reuse an inbound client token upstream, and it does not act on upstream sampling, roots, or elicitation.
Each call uses one guarded session and a broker-minted credential, then closes the session.
Managed MCP endpoints use variable("NAME") plus an exact allowedOrigins entry. Static upstream auth can
use no auth, bearer, API key, or client credentials. Per-user delegated upstream OAuth is not part of this
initial connector contract.
Compute connectors
When a bit of logic is genuinely needed (reshaping a response, deriving a value), a compute connector runs self-contained TypeScript in a sandbox with no ambient authority: no network, filesystem, secrets, or tokens.
const convert = connector('convert')
.version('1.0.0')
.compute('c_to_f', {
type: 'read',
input: z.object({ temp_c: z.number() }),
output: z.object({ temp_f: z.number() }),
run: (input) => ({ temp_f: input.temp_c * 1.8 + 32 }),
});The run function must be self-contained: no closures over outer variables, no imports, no fetch.
It reaches other operations only through an allowlisted host.callOperation, and you can bound it
with limits (timeout, memory, output size). Bind it on use and call it like any connector.
Binding connectors
useexposes a connector to your tools, callable asctx.connectors.<name>.<operation>().providesregisters a connector in the catalog for compute steps to reach viahost.callOperation, without exposing it directly to tools.
Import paths
noodle import mcp generates a governed frozen MCP connector. noodle import openapi generates HTTP
connector starter code from an OpenAPI document. Review either generated surface before publishing tools.
Designing tools for agents
Design useful MCP tools around user tasks with clear titles, behavior hints, bounded outputs, deliberate context, and a small surface.
Apps & widgets
Render React interfaces inside an MCP host and connect them to your tools. Learn widget authoring, resource delivery and the current host limitations.