# JavaScript Managed MCP Server

> For the complete documentation index, see [llms.txt](https://docs.redpanda.com/llms.txt). Component-specific: [agentic-data-plane-full.txt](https://docs.redpanda.com/agentic-data-plane-full.txt)

---
title: JavaScript Managed MCP Server
latest-operator-version: v26.2.3
latest-console-tag: v3.11.0
latest-connect-version: 4.108.0
latest-redpanda-tag: v26.2.2
docname: managed/javascript
page-component-name: agentic-data-plane
page-version: master
page-component-version: master
page-component-title: Agentic Data Plane
page-relative-src-path: managed/javascript.adoc
page-edit-url: https://github.com/redpanda-data/adp-docs/edit/main/modules/connect/pages/managed/javascript.adoc
description: Ship a custom MCP server by writing JavaScript. Redpanda runs your bundle in a sandbox and holds its requests to a host allowlist.
page-topic-type: how-to
personas: agent_builder, platform_engineer
learning-objective-1: Declare MCP tools in JavaScript against the <code>@redpanda-data/mcp-types</code> package and bundle them into a single file
learning-objective-2: Create the JavaScript managed MCP server in the UI or from the terminal, with an outbound host allowlist and secret references
learning-objective-3: Verify the discovered tools, replace the bundle in place, and connect an agent
page-git-created-date: "2026-08-21"
page-git-modified-date: "2026-09-03"
---

<!-- Source: https://docs.redpanda.com/agentic-data-plane/connect/managed/javascript.md -->

The **JavaScript** managed MCP server lets you ship a custom [MCP server](https://docs.redpanda.com/agentic-data-plane/reference/glossary/#mcp-server) by writing JavaScript, with no service of your own to build, host, or operate. Reach for it when the system you want an agent to reach is not in the managed catalog and the integration needs real logic.

After reading this page, you will be able to:

-   Declare MCP tools in JavaScript against the `@redpanda-data/mcp-types` package and bundle them into a single file

-   Create the JavaScript managed MCP server in the UI or from the terminal, with an outbound host allowlist and secret references

-   Verify the discovered tools, replace the bundle in place, and connect an agent


> 📝 **NOTE**
>
> The JavaScript managed MCP server is in preview. If you don’t see JavaScript in the picker, contact Redpanda support.

## [](#what-this-mcp-server-does)What this MCP server does

Every other managed type carries a fixed tool set that Redpanda maintains. This one has none: your bundle declares each [tool](https://docs.redpanda.com/agentic-data-plane/reference/glossary/#tool) at load time, and the server exposes exactly what it declared. Redpanda evaluates the bundle in a sandbox inside AI Gateway, so you build no container and expose no endpoint.

That suits work a fixed tool set cannot express: several upstream calls behind one tool, a computed signature, or a response you shape before an LLM reads it.

What the sandbox gives your code:

-   An async `fetch` and the standard web classes that go with it, held to the hostnames you allowlist.

-   Secret references your code reads by name, resolved to real values only where a credential belongs.

-   Web Crypto, text encoding, streams, timers, and the standard ECMAScript built-ins.


What it withholds:

-   A filesystem and raw sockets.

-   Module loading, including `require` and dynamic `import`.

-   Any state that survives a tool call. Each call starts from a clean evaluation of your bundle, so anything you want to keep between calls must live in the upstream system.


For an integration that only forwards operations from an OpenAPI description, [the OpenAPI managed type](https://docs.redpanda.com/agentic-data-plane/connect/managed/openapi/) needs no code at all. For logic that outgrows the sandbox limits, register [a self-managed server](https://docs.redpanda.com/agentic-data-plane/connect/register-remote/) instead.

## [](#prerequisites)Prerequisites

Before you write any code, make sure you have:

-   An Agentic Data Plane environment with access to create MCP servers. See [Create an MCP Server](https://docs.redpanda.com/agentic-data-plane/connect/create-server/).

-   [Node.js 20 or later](https://nodejs.org/en/download), to type-check, test, and bundle your handler.

-   A bundler. This page uses [esbuild](https://esbuild.github.io/).

-   An entry in **Secrets store** for each upstream credential your code needs. Secret names must be `UPPER_SNAKE_CASE`, for example `EXAMPLE_API_KEY`.

-   For the terminal workflow, the Agentic Data Plane CLI, signed in and pointed at your environment. See [Use the Agentic Data Plane CLI](https://docs.redpanda.com/agentic-data-plane/cli/).


## [](#write-the-handler)Write the handler

Your bundle is one script. Redpanda evaluates it to collect the tools it registers, and runs a tool’s handler when an agent calls that tool.

### [](#declare-tools)Declare tools

Install the types package as a development dependency. It ships type declarations only, no runtime code, so nothing from it ends up in your bundle:

```bash
npm install --save-dev @redpanda-data/mcp-types
```

Call `mcp.tool()` at module top level, one call per tool. You need no imports, because the sandbox supplies every global at load time:

```ts
/// <reference types="@redpanda-data/mcp-types" />

mcp.tool({
  name: 'get_user',
  description: 'Fetch a user by ID from the Example API.',
  inputSchema: {
    type: 'object',
    required: ['id'],
    properties: { id: { type: 'string', description: 'User ID' } },
  },
  outputSchema: {
    type: 'object',
    required: ['id', 'email'],
    properties: { id: { type: 'string' }, email: { type: 'string' } },
  },
  handler: async function (args: { id: string }) {
    const url = new URL(`https://api.example.com/users/${args.id}`);
    const resp = await fetch(url, {
      headers: { Authorization: `Bearer ${secrets['EXAMPLE_API_KEY']}` },
      signal: AbortSignal.timeout(5000),
    });
    if (!resp.ok) {
      throw new Error(`get_user: HTTP ${resp.status}`);
    }
    return resp.json();
  },
});
```

Registration rules to keep in mind:

-   Register at the top level of the script, not inside a function. A tool registered inside a handler is never found.

-   Keep top-level code to registration. During the discovery pass, `fetch`, `secrets`, `console`, and `crypto.subtle` all throw, and one call to any of them at the top level fails the whole evaluation, so the server ends up with no tools at all rather than one missing tool. Random values from `crypto.randomUUID()` and `crypto.getRandomValues()` are the exception, and work at the top level.

-   Tool names must match `^[a-zA-Z0-9_-]{1,64}$` and be unique. A bundle can register at most 64 tools.

-   Give every tool an `inputSchema`, as a JSON Schema object. When the return shape is known, add the optional `outputSchema` field, and the tool result carries parsed structured content alongside the JSON text.

-   Throw an error to fail a call. The message reaches the caller, so make it say which tool failed and why.

-   Console output never reaches the tool result, and managed deployments drop it. Thrown errors are the only diagnostics that reach you.


### [](#runtime-globals)Runtime globals

The sandbox exposes a standards-based subset of a modern JavaScript runtime, so code written against web APIs works without adaptation.

| Global | Notes |
| --- | --- |
| mcp.tool() | Tool registration. The only Redpanda-specific global. |
| fetch, Headers, Request, Response | WHATWG Fetch. Read response bodies as text, JSON, bytes, a blob, form data, or a stream. Requests go to https URLs only, and AI Gateway checks each one against the host allowlist. |
| secrets | Read a secret reference by name, such as secrets['EXAMPLE_API_KEY']. A name absent from Allowed Secrets throws rather than returning nothing. |
| URL, URLSearchParams | WHATWG URL. Build request URLs with these rather than string concatenation. |
| Blob, File, FormData | Binary and multipart request bodies. |
| ReadableStream, WritableStream, TransformStream | WHATWG Streams, with default readers. |
| AbortController, AbortSignal | Per-request timeouts and cancellation, including AbortSignal.timeout(). |
| crypto | Random values, UUIDs, and the Web Crypto subtle interface for digests, HMAC, signatures, and key derivation. |
| TextEncoder, TextDecoder, atob, btoa | Text encoding and base64. |
| console | Accepted and discarded in managed deployments, which run the gateway above debug level. Treat it as unavailable and report failures by throwing instead. |
| setTimeout, setInterval, queueMicrotask, structuredClone | Standard runtime helpers, alongside JSON, Math, Map, Set, Promise, and the other ECMAScript built-ins. |
| Date, performance | Present, but running on a deterministic clock. See the caution about time. |

> ⚠️ **CAUTION**
>
> The sandbox clock is deterministic, so `Date.now()` and `performance.now()` return an internal counter rather than the current time. Anything that needs a real timestamp, such as a request signature, an HMAC over a date header, or a JWT `iat` claim, must take it from somewhere else: a value the agent passes in as a tool argument, or a `Date` header on an earlier upstream response.

### [](#bundle-to-a-single-file)Bundle to a single file

Redpanda accepts one self-contained file, 1 MiB or smaller, with no dynamic imports. Bundle your source before you upload it:

```bash
npx esbuild src/index.ts \
  --bundle \
  --platform=neutral \
  --target=es2022 \
  --format=iife \
  --outfile=dist/index.js
```

Because `mcp`, `fetch`, and `secrets` are globals, you can test handlers locally by assigning your own stubs to `globalThis` and calling the registered handler directly, with no Redpanda environment involved.

## [](#create-the-server)Create the server

Two paths create the same resource: the create form and the CLI. Use the form to explore the fields, and the CLI to rebuild a server from a bundle you keep in version control.

### [](#create-in-the-ui)Create in the UI

1.  Open **MCP Servers > Add MCP server**.

2.  Pick **JavaScript** from the marketplace picker. It sits under the **Utilities** filter.

3.  Under **Identity**, set `Name`. A name starts with a lowercase letter, continues with lowercase letters, numbers, and hyphens, and runs to at most 63 characters. Add a `Description` to say what the server does.

4.  Under **Configuration**, load your bundle into `Code`. Drop a `.js`, `.mjs`, or `.cjs` file onto the upload area, or paste the code into the editor. The editor reports syntax errors as you type, with the line and column.

5.  If you want AI Gateway to attach the upstream credential for you, set `Auth`. For a public API, or when your code reads secret references itself, leave it at `Not set`. See [Authenticate outbound requests](#authenticate-outbound-requests).

6.  Add each hostname your code calls to `Allowed Hosts`. See [Control outbound network access](#control-outbound-network-access).

7.  Add each secret name your code reads to `Allowed Secrets`. The field takes names, not values: create the secrets in **Secrets store** first.

8.  Check the request preview pane, which shows the exact configuration body the form submits.

9.  Click **Create server**.


The form leaves `Code mode` on for this type, which serves a sibling endpoint that lets an agent run sandboxed code against this server’s tools. See [Code Mode](https://docs.redpanda.com/agentic-data-plane/gateway/code-mode/).

### [](#create-from-the-terminal)Create from the terminal

Pass the bundle and its configuration as one `--managed.config` document. Build the document with `jq` so the code is escaped correctly:

```bash
rpk ai mcp-server create example-glue \
  --enabled \
  --managed.config "$(jq -nc --rawfile code dist/index.js '{
    "@type": "JavaScriptMCP",
    code: $code,
    allowedHosts: ["api.example.com"],
    allowedSecrets: ["EXAMPLE_API_KEY"]
  }')"
```

Replace `example-glue` with the name for your server, `api.example.com` with the hostnames your code calls, and `EXAMPLE_API_KEY` with your secret names.

Five details matter in that command:

-   Pass `--enabled`. A server created without it is disabled, and every call to it fails to connect until you run `rpk ai mcp-server update <server-name> --enabled`, where `<server-name>` is the name you gave the server.

-   Pass `--code-mode` if you want code mode. The create form leaves it on for this type, but the CLI leaves it off unless you ask for it, so the same configuration creates a server without the search and execute pair when you build it from the terminal.

-   The `@type` field accepts the short type name, `JavaScriptMCP`, or the full type URL, `type.googleapis.com/redpanda.mcps.javascript.v1.JavaScriptMCPConfig`. Run `rpk ai mcp-server types` to list the short names your environment serves.

-   Field names accept `camelCase` or the underlying `snake_case` spelling, such as `allowed_hosts`.

-   Add `--dry-run` to print the request without sending it.


Creation runs two checks that fail differently. The configuration is validated outright: a missing bundle, a malformed host pattern, or secrets without hosts fails the command. The bundle is then evaluated with a five-second budget, and that pass is best-effort. A script that throws, registers no tools, or overruns the budget still returns a successful create, with an empty tool list. A server created without `--enabled` is not evaluated at all.

To keep the server in version control, dump it as a manifest with `rpk ai mcp-server get <server-name> -o yaml`, then reconcile it with `rpk ai mcp-server apply -f <manifest-file>` and preview changes with `rpk ai mcp-server diff -f <manifest-file>`. Replace `<manifest-file>` with the path to the dumped manifest.

## [](#authenticate-outbound-requests)Authenticate outbound requests

Upstream credentials reach the system in one of two ways. Pick one per credential: a gateway-attached credential overwrites what your code sets. A server can still use both mechanisms for different credentials.

### [](#let-ai-gateway-attach-the-credential)Let AI Gateway attach the credential

Set `Auth` and the gateway adds the credential to every outbound request. Your code never sees the value:

| Mode | Use when |
| --- | --- |
| Not set | The upstream needs no credential, or your code attaches one itself from a secret reference. |
| Static Key | The upstream takes a bearer token. Set Key Secret Ref to the secret name. Header Name moves the token to another header, defaulting to Authorization, but the value always carries the Bearer prefix: naming a custom header sends Bearer <token> in that header, not the bare token. For an upstream that wants a bare token, or a token in a query parameter, read a secret reference in code instead. |
| Basic Auth | The upstream takes HTTP Basic credentials. Set the username and a secret reference for the password. |
| OAuth | The upstream issues tokens through the OAuth client-credentials grant, and one shared service-account identity fits every caller. Set the client ID, a secret reference for the client secret, the token URL, and any scopes. The gateway runs the exchange, refreshes the token, and attaches it. Put the token endpoint’s hostname in Allowed Hosts. |
| User OAuth | Each agent caller must reach the upstream as themselves. Set the OAuth provider whose per-user tokens authenticate the requests. See Configure User-Delegated OAuth. |

An attached credential wins: it overwrites an `Authorization` header your code sets.

### [](#attach-the-credential-in-code)Attach the credential in code

List a secret name in `Allowed Secrets` and read it as `secrets['NAME']`. You get back a placeholder, not the secret itself, and the gateway substitutes the real value in three positions only: a request header value, the request URL, and raw key material passed to `crypto.subtle.importKey()`. Everywhere else the placeholder stays opaque, and a placeholder in a request body fails the request. Use this mechanism when the upstream wants the credential in a custom header, in a query parameter, or as an HMAC key.

The `importKey()` position is narrower than the other two. A secret reference resolves there only for a `raw` import, and only for the symmetric and key-derivation algorithms: `HMAC`, `AES-GCM`, `AES-CBC`, `AES-CTR`, `AES-KW`, `HKDF`, and `PBKDF2`. Any other format, such as `pkcs8` or `spki`, and any other algorithm, including the RSA and elliptic-curve families, refuses the reference with a `DataError`. Those imports expect public key material rather than a credential, so pass the bytes directly instead.

Because a secret reference is only useful on an outbound request, `Allowed Secrets` requires at least one entry in `Allowed Hosts`.

## [](#control-outbound-network-access)Control outbound network access

`Allowed Hosts` is the full list of hostnames your code can reach. A request to anything else fails, so an upstream you forget to list shows up as a tool error rather than an unnoticed call.

How Redpanda matches entries:

-   Write a bare hostname, such as `api.example.com`, or a wildcard that covers one extra label, such as `*.example.com`. Matching ignores case.

-   List a parent domain in its own right if you call it. A wildcard covers only the label below it, so `*.example.com` matches `api.example.com` but not `example.com` itself, and not a deeper name such as `api.eu.example.com`.

-   Reach every host over `https`. An `http` URL fails before the allowlist is consulted, and the allowlist entry itself carries no scheme.

-   Leave out the scheme, the port, and the path. A URL in this field fails validation.

-   A configuration can list at most 32 hostnames and 16 secret names.


Three guarantees hold regardless of what you list. Private and reserved address space stays unreachable, including loopback, link-local, and cloud metadata addresses. The check runs against the address the connection actually resolves to, so a hostname that resolves differently on the second lookup gains nothing. AI Gateway checks every redirect hop again, so a redirect off the allowlist fails the same way a direct request would.

## [](#verify-the-tools)Verify the tools

Check the tool list before you point an agent at the server. AI Gateway evaluates the bundle when you create or update the server, so an empty list means the script failed to load, not that discovery has yet to run.

From the UI, open the server’s **Inspector** tab, pick a tool, and run it. The **Overview** tab lists the discovered tools with their descriptions. See [Test an MCP Server’s Tools with the Inspector](https://docs.redpanda.com/agentic-data-plane/connect/test-tools/).

From the terminal, list what the server discovered, then call one tool:

```bash
rpk ai mcp-server tools list example-glue
rpk ai mcp-server tools call example-glue get_user --args '{"id":"42"}'
```

A tool that declares an `outputSchema` returns both the JSON text and parsed structured content, so agents that read structured output can address fields directly.

## [](#update-the-bundle)Update the bundle

A bundle is not frozen after creation. Rebuild, then replace the configuration in place:

```bash
npm run build
rpk ai mcp-server update example-glue \
  --managed.config "$(jq -nc --rawfile code dist/index.js '{
    "@type": "JavaScriptMCP",
    code: $code,
    allowedHosts: ["api.example.com"],
    allowedSecrets: ["EXAMPLE_API_KEY"]
  }')"
```

The document replaces the whole configuration, so include the hosts and secrets you want to keep. In the UI, **Edit** does the same thing.

Allow a few seconds before you call the new tools. AI Gateway keeps warm instances of each server and reconciles them against stored configuration every 10 seconds, so a call made immediately after an update can still run the previous bundle.

## [](#use-with-an-agent)Use with an agent

An agent reaches the tools by referencing the server by name. The server must already exist in the same environment:

```bash
rpk ai agent create support-agent \
  --model <model-id> \
  --llm-provider <provider-name> \
  --system-prompt "You answer questions about users in the Example API." \
  --mcp-server example-glue
rpk ai agent start support-agent
```

In this command, `<model-id>` is a model your LLM provider serves, and `<provider-name>` is that provider’s resource name. The `--mcp-server` flag is repeatable and replaces the agent’s server list on each create or update, so pass every server the agent needs in one command. The managed-agent create and edit forms carry the same setting. See [Create an Agent](https://docs.redpanda.com/agentic-data-plane/connect/create-agent/).

Write tool descriptions and input schemas for the model, not for a human reader. A description that says which identifier a tool expects and an `outputSchema` that names the fields it returns do more for tool selection than any system prompt.

## [](#sandbox-limits)Sandbox limits

These limits are fixed. Design tools to fit inside them: one focused upstream call per tool beats a tool that walks a paginated collection.

| Scope | Limit | Applies to |
| --- | --- | --- |
| Memory | 10 MiB | Each tool call. |
| Execution time | 10 seconds | Each tool call, including work still pending in promises. |
| Single request | 8 seconds | Each fetch, counted across a streamed body. |
| Response size | 5 MiB | Each fetch response. |
| Bundle size | 1 MiB | The uploaded file, which must be self-contained. |
| Tools | 64 | Each bundle. |
| Hosts and secrets | 32 and 16 | Each configuration. |
| Concurrent calls | 5 | Each server. A sixth call waits for a sandbox to free up. |

## [](#troubleshooting)Troubleshooting

Common symptoms and fixes:

| Symptom | What to check |
| --- | --- |
| The Overview tab reports that tools have not been discovered yet | The bundle registered no tools. Confirm mcp.tool() runs at module top level, and that registration doesn’t depend on fetch or secrets, which are stubbed during discovery. |
| A tool call fails with host not in allowlist: <hostname> | Add the hostname to Allowed Hosts. A wildcard covers one extra label, so *.example.com matches api.example.com but neither example.com itself nor api.eu.example.com. A redirect to an unlisted host fails the same way. |
| Creation fails with Add at least one allowed host when allowed secrets are configured. | Secret references only resolve on outbound requests. Add the upstream hostnames. |
| Creation fails with a pattern error on allowed_hosts | An entry carries a scheme, a port, or a path. Use a bare hostname or a *.suffix wildcard. |
| Every call to a new server fails to connect | A server created from the terminal without --enabled is disabled. Run rpk ai mcp-server update <server-name> --enabled. |
| A secret reads back as an opaque placeholder rather than its value | Expected. The value appears in a header value, in the URL, and in a raw crypto.subtle.importKey() for a symmetric or key-derivation algorithm, and stays a placeholder anywhere else. An importKey() that refuses the reference outright, with a DataError, is using a different format or algorithm family. |
| The Authorization header your code sets doesn’t arrive | An attached credential from Auth overwrites it. Drop the header, or set Auth to Not set and attach the credential in code. |
| A request fails with HTTPS required | The upstream URL uses http. Only https requests leave the sandbox, and that includes an OAuth token URL. |
| A signature or token the upstream rejects as expired or out of date | The sandbox clock is deterministic, so a timestamp taken from Date is not the current time. Take it from a tool argument or an upstream response header. |
| A call fails after a long pause | The call hit the execution-time or request limits. Narrow the upstream request, or split the work across tools. |
| New code doesn’t take effect | Warm instances reconcile every 10 seconds. Wait a few seconds and call the tool again. |

## [](#limitations)Limitations

-   **Stateful sessions**: No state carries between tool calls. Keep session state in the upstream system.

-   **Credentials in a request body**: A secret reference resolves in a header, in the URL, or as raw symmetric key material, never in a body. Compute a signature with `crypto.subtle` and send the result instead.

-   **Modules and packages**: The sandbox loads one self-contained file. Bundle every dependency, and expect no filesystem or network access beyond `fetch`.

-   **Long-running work**: A tool call that cannot finish inside the execution-time limit in the Sandbox limits table belongs in [a self-managed server](https://docs.redpanda.com/agentic-data-plane/connect/register-remote/).


## [](#next-steps)Next steps

-   [Test a server’s tools](https://docs.redpanda.com/agentic-data-plane/connect/test-tools/)

-   [Create an agent](https://docs.redpanda.com/agentic-data-plane/connect/create-agent/)

-   [Plug in an App, Database, or Tool](https://docs.redpanda.com/agentic-data-plane/connect/managed/managed-catalog/)