Jangada AIJangada AI

MCP

The jangada-ai/mcp subpath covers both sides of the Model Context Protocol: consuming MCP servers and exposing your tools as a server. On Node, serveMcp (stdio or HTTP) lives in jangada-ai/node.

npm install jangada-ai @modelcontextprotocol/sdk
npm install quickjs-emscripten   # Code Mode only

Both packages are optional peers. The SDK is only loaded when you connect or build a server, so import "jangada-ai/mcp" works without it.

Two models

ModelWho runs the toolsProviders
Remote: new MCPServer({ url, name }) in mcpServersThe provider, on its serversAnthropic, OpenAI, Azure, Groq. On Gemini, jangada opens the connection itself during the call.
Client-side: MCPClient + runAgent or AgentYour applicationAny provider with tools

OpenRouter and DeepSeek reject mcpServers with UnsupportedError. Use the client-side model with them. On Gemini, mcpServers also accepts an already connected MCPClient: jangada calls the tools locally during the request.

Remote (the provider connects)

import { LLM } from "jangada-ai";
import { MCPServer } from "jangada-ai/mcp";

const llm = new LLM("anthropic", "claude-sonnet-4-5");
const comp = await llm.complete("List the products in stock.", {
  mcpServers: [
    new MCPServer({
      url: "https://your-mcp.example/mcp",
      name: "inventory",
      authorizationToken: process.env.MCP_TOKEN,
      allowedTools: ["list_products"],
    }),
  ],
});

requireApproval ("never" by default, or "always") applies to OpenAI, Azure and Groq. The token lives in a private field and does not show up in JSON.stringify or in error messages.

MCP client + agent (any provider)

MCPClient takes a URL (streamable HTTP; SSE if the path ends in /sse) or a stdio command. The constructor does not connect: connect() does. With await using, the connection closes at the end of the block.

import { LLM } from "jangada-ai";
import { MCPClient, runAgent } from "jangada-ai/mcp";

const llm = new LLM("openai", "gpt-4o-mini");

await using mcp = await new MCPClient("https://your-mcp.example/mcp", {
  authToken: process.env.MCP_TOKEN,
  timeout: 30_000,
}).connect();

const r = await runAgent(llm, "Which products are running low?", {
  client: mcp,
  allowedTools: ["list_products"],
  onToolCall: (call) => console.log("calling", call.name),
});
console.log(r?.text, r?.toolTrace, r?.costTotal);

stdio (Node, Bun and Deno only):

await using fs = await new MCPClient({
  command: "npx",
  args: ["-y", "@modelcontextprotocol/server-filesystem", "."],
}).connect();

MCPClient options:

OptionWhat it does
url / command, args, env, cwdHTTP target or stdio subprocess.
headers, authTokenExtra headers. authToken becomes Authorization: Bearer.
transportForces "sse" or "streamable-http".
timeoutTimeout for each request, in ms.
keepAliveConnects on the first call and reconnects once if the connection drops.
retryToolsWith keepAlive, also retries callTool on read errors. Only for idempotent tools.
roots, samplingLlm, onSamplingRequest, elicitationCallback, loggingCallbackClient capabilities: roots, sampling (with approval), elicitation and server logs.

For a long-lived connection outside await using, call connect() at startup and close() at the end. There are also reconnect(), ping() and the connected getter. A connection failure becomes APIConnectionError.

Protocol primitives

const tools = await mcp.listTools();
const res = await mcp.callTool("add", { a: 2, b: 3 }); // res.isError = the tool failed
const text = await mcp.resourceText("file:///guide.md");
const history = await mcp.promptMessages("summarize", { topic: "jangadas" });

There are also listResources, readResource, listPrompts, getPrompt and setLoggingLevel. Listings follow the server's pagination.

runAgent

runAgent(llm, prompt, options) lists the tools, runs the loop and returns the final Completion with toolTrace, iterations, stoppedByLimit, usageTotal, costTotal and costComplete. It returns null if maxIterations <= 0.

OptionWhat it does
clientThe MCPClient (or any object with listTools/callTool). Required.
maxIterationsDefault 10.
system, history, params, signalPassed to every complete().
allowedToolsFilters the offered tools and the executable ones. A tool outside the list goes back to the model as an error.
onToolCall, onToolResultReturning false from onToolCall vetoes the call.
toolsTools already listed with mcpTools(client), to skip listTools().

To use MCP inside an Agent, pass mcpClient: mcp (see Agents).

MCP server

buildMcpHandler(tools, options) returns a web-standard handler (req: Request) => Promise<Response> over Streamable HTTP. It works on Node, Bun, Deno, Cloudflare Workers, Hono and Next route handlers.

import * as z from "zod";
import { tool } from "jangada-ai";
import { buildMcpHandler } from "jangada-ai/mcp";

const add = tool({
  name: "add",
  description: "Adds two numbers.",
  parameters: z.object({ a: z.number(), b: z.number() }),
  execute: ({ a, b }) => a + b,
});

const handler = buildMcpHandler([add], {
  name: "calc",
  stateless: true,
  authToken: process.env.MCP_TOKEN,
});
export default { fetch: handler };

On Node, serveMcp starts the server over stdio (Claude Desktop, Cursor) or HTTP. It resolves once the server is up and returns a close():

import { serveMcp } from "jangada-ai/node";

const srv = await serveMcp([add], { transport: "http", port: 3333 });
console.log(srv.url); // http://127.0.0.1:3333/mcp

Server options:

OptionWhat it does
name, title, version, websiteUrl, iconsserverInfo data.
statelessOne server per request, no session. Use it on serverless and with per-user filters. Default false.
jsonResponseJSON responses instead of SSE. Default true.
pathOnly serves this path (serveMcp uses /mcp).
authTokenRequires Authorization: Bearer <token>.
allowedHosts, allowedOriginsDNS rebinding protection.
toolFilter(ctx)Names allowed on this request (null allows all).
toolDescription(name, description, ctx)Swaps the description per request.
codeModeTurns on Code Mode (below).

Without authToken or allowedHosts/allowedOrigins the endpoint is open. The library does not do OAuth or CORS.

Who is calling

Inside a tool served over HTTP, requestHeaders() returns the request headers and currentUser() reads the identity sent by the jangada Cloud gateway (x-jangada-user-*, x-jangada-claims). Both use AsyncLocalStorage when the runtime has it. The tool's execute also receives the context as its 2nd argument, which works on any runtime.

import { currentUser, requestHeaders } from "jangada-ai/mcp";

const whoami = tool({
  name: "whoami",
  description: "Shows the calling user.",
  execute: () => currentUser()?.email ?? requestHeaders()["x-tenant"] ?? "anonymous",
});

On a runtime without AsyncLocalStorage, pass the context execute receives: execute: (args, ctx) => currentUser(ctx)?.email. The type takes the tool's ctx directly, with no cast.

currentUser() returns null on stdio and outside jangada Cloud. On a server exposed directly, any client can send those headers.

Code Mode

With many tools, codeMode: true replaces the catalog with three meta-tools: search, get_schema and execute. execute runs JavaScript in a QuickJS (WebAssembly) sandbox where the code calls await callTool(name, params). The sandbox has no fetch, require, process, filesystem or timers.

import { buildMcpHandler, CodeMode } from "jangada-ai/mcp";

const h1 = buildMcpHandler(tools, { codeMode: true });
const h2 = buildMcpHandler(tools, {
  codeMode: new CodeMode({
    limits: { timeoutMs: 10_000, memoryBytes: 50_000_000 },
    direct: ["cancel_order"], // stays outside the sandbox, as a regular tool
  }),
});
CodeMode optionWhat it does
limitstimeoutMs (default 30000), memoryBytes (default 100 MB), maxStackBytes.
searchLimitMax search results. Default 8.
discovery: falseNo search/get_schema: the catalog goes into the execute description.
directTools kept outside the sandbox. The client then asks for human approval on each of their calls.
executeDescription, executeAnnotationsDescription and MCP annotations of execute.
quickjsAn already loaded QuickJS module. On Cloudflare Workers, pass one built with newQuickJSWASMModuleFromVariant().

toolFilter also applies inside the sandbox: search, get_schema and callTool only see the tools allowed on the request.

Differences from Python

  • In Python, Code Mode runs Python on Monty. Here it runs JavaScript on QuickJS.
  • serve_mcp blocks in Python. serveMcp returns a Promise with close().
  • Instead of the Starlette app (build_mcp_app), JS has the Request => Response handler.
  • Python's async with MCPClient(...) becomes await using mcp = await new MCPClient(...).connect().
  • Timeouts in milliseconds.

On this page