Skip to content

Tool or MCP?

Capability design Security-sensitive

The short rule is:

  • Use a JavaScript Tool for an application-owned function that should run in the host process with a typed input contract.
  • Use MCP for a separately deployed or shared capability server whose transport, credentials, and lifecycle are managed as an external integration.

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.

QuestionJavaScript ToolMCP
Where does code run?In the application host processIn a stdio process or remote Streamable HTTP server owned by the integration
Best forRepository/database/domain functions already owned by the applicationShared, independently deployed, or cross-language capability servers
ContractCallable returned by defineTool(), with input schema and JSON-compatible resultdefineMcpServer() transport descriptor; provider negotiates the server
Credential ownerApplication code and its dependency injection boundaryMCP server or transport configuration; often a token/header or process environment
Failure boundaryTool executor, validation, cancellationTransport, server availability, authentication, and provider relay
Sandbox implicationHost-process code is not sandboxed by an Agent’s SandboxRemote MCP trust and egress remain deployment concerns; a Sandbox does not make the server trustworthy
Choose this whenYou need direct typed access to an application-owned dependencyYou 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 authorization

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

A complete application-owned JavaScript Tool

Section titled “A complete application-owned JavaScript Tool”

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_MODEL
const openAiKey = process.env.OPENAI_API_KEY
const 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.