Noodle Seed
Guides

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.

src/server.ts
import { server, tool, connector, secret, z } from '@noodleseed/one';

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.city}' },
        input: z.object({ city: z.string() }),
        output: z.object({ temp_c: z.number() }),
        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.',
    input: z.object({ city: z.string() }),
    output: z.object({ temp_c: z.number() }),
    fulfil: ({ input, connectors }) => connectors.weather.current({ city: input.city }),
  }),
]);

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 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 with noodle secrets set.
  • variable("NAME") for non-secret config (a base URL, a tenant id). Set with noodle 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. Per-user delegated auth kinds are Roadmap (Phase 3).

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:

ExpressionResolves to
${input.x}A field of the 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/${input.id}',                 // interpolate input into the path
    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. Per-user ${user.*} expressions are Roadmap (Phase 3).

Upstream MCP connectors

Shipped

Adopt an existing remote Streamable HTTP MCP server with an explicit, reviewable snapshot:

noodle import mcp https://store.example/api/mcp \
  --name store \
  --output store-assistant

The 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.

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_TOKEN

Use --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 \
  --check

The 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

  • use exposes a connector to your tools, callable as ctx.connectors.<name>.<operation>().
  • provides registers a connector in the catalog for compute steps to reach via host.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.

On this page