JavaScript Managed MCP Server
The JavaScript managed MCP server lets you ship a custom 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-typespackage 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
|
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
Every other managed type carries a fixed tool set that Redpanda maintains. This one has none: your bundle declares each 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
fetchand 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
requireand dynamicimport. -
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 needs no code at all. For logic that outgrows the sandbox limits, register a self-managed server instead.
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.
-
Node.js 20 or later, to type-check, test, and bundle your handler.
-
A bundler. This page uses esbuild.
-
An entry in Secrets Store for each upstream credential your code needs. Secret names must be
UPPER_SNAKE_CASE, for exampleEXAMPLE_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.
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
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:
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:
/// <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, andcrypto.subtleall 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 fromcrypto.randomUUID()andcrypto.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 optionaloutputSchemafield, 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
The sandbox exposes a standards-based subset of a modern JavaScript runtime, so code written against web APIs works without adaptation.
| Global | Notes |
|---|---|
|
Tool registration. The only Redpanda-specific global. |
|
WHATWG Fetch. Read response bodies as text, JSON, bytes, a blob, form data, or a stream. Requests go to |
|
Read a secret reference by name, such as |
|
WHATWG URL. Build request URLs with these rather than string concatenation. |
|
Binary and multipart request bodies. |
|
WHATWG Streams, with default readers. |
|
Per-request timeouts and cancellation, including |
|
Random values, UUIDs, and the Web Crypto |
|
Text encoding and base64. |
|
Accepted and discarded in managed deployments, which run the gateway above debug level. Treat it as unavailable and report failures by throwing instead. |
|
Standard runtime helpers, alongside |
|
Present, but running on a deterministic clock. See the caution about time. |
|
The sandbox clock is deterministic, so |
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:
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
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
-
Open MCP Servers > Add MCP server.
-
Pick JavaScript from the marketplace picker. It sits under the Utilities filter.
-
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 aDescriptionto say what the server does. -
Under Configuration, load your bundle into
Code. Drop a.js,.mjs, or.cjsfile onto the upload area, or paste the code into the editor. The editor reports syntax errors as you type, with the line and column. -
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 atNot set. See Authenticate outbound requests. -
Add each hostname your code calls to
Allowed Hosts. See Control outbound network access. -
Add each secret name your code reads to
Allowed Secrets. The field takes names, not values: create the secrets in Secrets Store first. -
Check the request preview pane, which shows the exact configuration body the form submits.
-
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.
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:
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 runrpk ai mcp-server update <server-name> --enabled, where<server-name>is the name you gave the server. -
Pass
--code-modeif 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
@typefield accepts the short type name,JavaScriptMCP, or the full type URL,type.googleapis.com/redpanda.mcps.javascript.v1.JavaScriptMCPConfig. Runrpk ai mcp-server typesto list the short names your environment serves. -
Field names accept
camelCaseor the underlyingsnake_casespelling, such asallowed_hosts. -
Add
--dry-runto 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
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
Set Auth and the gateway adds the credential to every outbound request. Your code never sees the value:
| Mode | Use when |
|---|---|
|
The upstream needs no credential, or your code attaches one itself from a secret reference. |
|
The upstream takes a bearer token. Set |
|
The upstream takes HTTP Basic credentials. Set the username and a secret reference for the password. |
|
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 |
|
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
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
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.commatchesapi.example.combut notexample.comitself, and not a deeper name such asapi.eu.example.com. -
Reach every host over
https. AnhttpURL 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
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.
From the terminal, list what the server discovered, then call one tool:
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
A bundle is not frozen after creation. Rebuild, then replace the configuration in place:
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
An agent reaches the tools by referencing the server by name. The server must already exist in the same environment:
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.
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
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 |
Response size |
5 MiB |
Each |
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
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 |
A tool call fails with |
Add the hostname to |
Creation fails with |
Secret references only resolve on outbound requests. Add the upstream hostnames. |
Creation fails with a pattern error on |
An entry carries a scheme, a port, or a path. Use a bare hostname or a |
Every call to a new server fails to connect |
A server created from the terminal without |
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 |
The |
An attached credential from |
A request fails with |
The upstream URL uses |
A signature or token the upstream rejects as expired or out of date |
The sandbox clock is deterministic, so a timestamp taken from |
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
-
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.subtleand 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.