Use a Tool
The operation is small, application-owned, typed, and safe to run in the host process with explicit dependency injection and authorization.
The short rule is:
Neither choice makes model input trusted. When a model should receive either capability, grant it lexically to the nearest <Agent /> and constrain grants with runtime allowlists. A JavaScript Tool can instead be called directly by active application component code without any model grant; an MCP descriptor is not an application-call API.
| Question | JavaScript Tool | MCP |
|---|---|---|
| Where does code run? | In the application host process | In a stdio process or remote Streamable HTTP server owned by the integration |
| Best for | Repository/database/domain functions already owned by the application | Shared, independently deployed, or cross-language capability servers |
| Contract | Callable returned by defineTool(), with input schema and JSON-compatible result | defineMcpServer() transport descriptor; provider negotiates the server |
| Credential owner | Application code and its dependency injection boundary | MCP server or transport configuration; often a token/header or process environment |
| Failure boundary | Tool executor, validation, cancellation | Transport, server availability, authentication, and provider relay |
| Sandbox implication | Host-process code is not sandboxed by an Agent’s Sandbox | Remote MCP trust and egress remain deployment concerns; a Sandbox does not make the server trustworthy |
| Choose this when | You need direct typed access to an application-owned dependency | You need a separately operated capability boundary |
Runtime allowlists allowedTools: ["list_orders"] allowedMcpServers: ["billing"] │ ▼ ┌───────────────────────────────┐ │ Agent │ │ ├─ <Tool use={listOrders} /> │──► host process function │ └─ <Mcp use={billing} /> │──► ACP/provider relay ──► billing server └───────────────────────────────┘ │ ├─ child <Agent /> does not inherit either capability ├─ Sandbox controls selected execution environment, not Tool trust └─ Workspace controls files and revisions, not MCP authorizationallowedTools and allowedMcpServers are deny-by-default constraints on the names that may be attached during evaluation. They do not replace input validation, server authentication, network policy, or application authorization.
This example is deterministic and credential-free. The repository owns SessionRepository; the model-facing capability exposes only the narrow list_session_orders operation.
import { Agent, AmlRuntime, defineTool, Tool } from "@aml-jsx/sdk"import { DeterministicAgentProvider } from "@aml-jsx/sdk/testing"import { z } from "zod"
class SessionRepository { async listOrders(): Promise<readonly string[]> { return ["order-17", "order-29"] }}
const provider = new DeterministicAgentProvider({ async respond(request, context) { const tool = request.tools.find(candidate => candidate.name === "list_session_orders")
if (tool?.kind !== "javascript") { throw new Error("Session Tool was not granted") }
const orders = await tool.execute({}, { signal: context.signal, trace: context.trace }) return { text: `orders:${JSON.stringify(orders)}` } },})
function OrderAgent({ repository }: { repository: SessionRepository }) { const listOrders = defineTool({ description: "List orders for the active application session.", execute: async (_input, context) => await repository.listOrders(), input: z.object({}), name: "list_session_orders", })
return ( <Agent provider={provider}> <Tool use={listOrders} /> Inspect the active session orders. </Agent> )}
const result = await new AmlRuntime({ allowedTools: ["list_session_orders"] }).evaluate( <OrderAgent repository={new SessionRepository()} />)
console.log(result)The deterministic provider uses .execute(input, context) because it receives AML’s provider-facing execution port. Within an active component that owns or receives the authored Tool, call it directly:
const orders = await listOrders({})That application call does not attach the Tool to an Agent and is not governed by allowedTools, which restricts model grants.
This example uses the maintained OpenCode ACP provider with a real Streamable HTTP MCP server. It requires an OpenCode executable, an OpenCode model credential, network access to the MCP endpoint, and an MCP token. The endpoint below is an application placeholder, not a public demo server.
import { Agent, AmlRuntime, Mcp, defineMcpServer, opencodeAgent } from "@aml-jsx/sdk"
const model = process.env.OPENCODE_MODELconst openAiKey = process.env.OPENAI_API_KEYconst mcpToken = process.env.BILLING_MCP_TOKEN
if (model === undefined || openAiKey === undefined || mcpToken === undefined) { throw new Error("OPENCODE_MODEL, OPENAI_API_KEY, and BILLING_MCP_TOKEN are required")}
const billing = defineMcpServer({ name: "billing", transport: { type: "streamable-http", // PLACEHOLDER: replace with the HTTPS endpoint operated by your team. url: "https://billing-mcp.example.invalid/mcp", headers: { Authorization: `Bearer ${mcpToken}` }, },})
const result = await new AmlRuntime({ allowedMcpServers: ["billing"], agentProvider: opencodeAgent({ command: "opencode", env: { OPENAI_API_KEY: openAiKey }, model, }),}).evaluate( <Agent system="Use billing only to inspect the account and never mutate invoices."> <Mcp use={billing} /> Inspect the account's outstanding invoices and summarize them. </Agent>)
console.log(result)The OpenCode profile launches opencode acp --pure --cwd ... and AML’s ACP engine relays configured MCP requests through an invocation-owned bridge. The MCP server still owns its authentication and authorization; the runtime allowlist only constrains the server name AML may attach.
Tools and MCP return capability data to <Agent />. Structured output validates the final <Agent /> result against a schema; it does not authorize the Tool, authenticate the MCP server, or prove that returned facts are true.
import { Agent, evaluate, Tool, defineTool } from "@aml-jsx/sdk"import { z } from "zod"
const InvoiceSummary = z.object({ invoiceIds: z.array(z.string()), totalCents: z.number().int().nonnegative(),})
const summary = await evaluate( <Agent> {/* Tools/MCP may provide evidence; application code still owns policy. */} Summarize the evidence as invoice IDs and a total in the requested schema. </Agent>, InvoiceSummary)Validate authorization and business invariants after schema validation. A correctly shaped { invoiceIds, totalCents } object can still contain incomplete or incorrect model-derived claims.
Use a Tool
The operation is small, application-owned, typed, and safe to run in the host process with explicit dependency injection and authorization.
Use MCP
The capability has an independent deployment or lifecycle, needs a standard transport, or is shared across clients and languages.
Use neither yet
The boundary, owner, credential scope, or failure behavior is unclear. Define those before exposing a model-facing capability.