This skill helps an LLM build correct native Model Context Protocol integrations with @ax-llm/ax. Use when the user asks about AxMCPClient, MCP transports, tools, prompts, resources, subscriptions, tasks, sampling, elicitation, roots, authentication, OAuth, MCP Apps, recording/replay, or MCP integration with AxGen, AxAgent, AxFlow, chat, optimization, and AxEventRuntime.
69
85%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Use MCP as a live protocol client, not as a function-conversion utility. Keep the client, session, catalogs, raw content, tasks, notifications, identity policy, and cancellation context intact through Ax execution.
mcp; do not put them in functions.toFunction() for native integration. It is a lossy compatibility
adapter for old applications only.namespace.era on 'auto' unless deployment policy pins a known legacy or
modern endpoint.authorizeToolCall before side-effecting tools execute.AxMCPStreamableHTTPTransport for current remote MCP servers.AxMCPHTTPSSETransport only for legacy HTTP/SSE servers.AxMCPWebSocketTransport for a server with a custom WebSocket binding.AxMCPStdioTransport from @ax-llm/ax-tools for local Node processes.AxMCPTransport for application-owned bindings.import {
AxMCPClient,
AxMCPStreamableHTTPTransport,
axMCPBearerAuthentication,
} from '@ax-llm/ax';
const transport = new AxMCPStreamableHTTPTransport(
'https://mcp.example.com/mcp',
{
authentication: axMCPBearerAuthentication(
() => process.env.MCP_ACCESS_TOKEN!
),
}
);
const docs = new AxMCPClient(transport, {
namespace: 'docs',
maxConcurrency: 4,
authorizeToolCall: async ({ tool }) =>
tool.annotations?.destructiveHint !== true,
});The TypeScript client supports two wire models through one API:
2025-11-25) initializes a stateful session, sends the
initialized notification, uses resource subscribe/unsubscribe requests, and
may resume GET/SSE with Last-Event-ID.2026-07-28) is stateless. It probes server/discover, sends
protocol metadata and routing headers on every request, listens through a
long-running subscriptions/listen POST, and uses Tasks v2.Automatic classification is the default and can be persisted with eraStore.
Pin era: 'legacy' or era: 'modern' only when the endpoint contract is
known. getEra() reports the classified era after initialization.
const modern = new AxMCPClient(transport, {
namespace: 'inventory',
era: 'auto',
readCache: true,
logLevel: 'info',
});
const discovery = await modern.discover();
console.log(modern.getEra(), discovery.supportedVersions);discover() performs initialization/classification but returns only for a
modern endpoint. For era-neutral application code, use inspectCatalog(); it
works in both eras.
For local stdio:
import { AxMCPClient } from '@ax-llm/ax';
import { AxMCPStdioTransport } from '@ax-llm/ax-tools';
const stdio = new AxMCPStdioTransport({
command: 'node',
args: ['./server.mjs'],
});
const local = new AxMCPClient(stdio, { namespace: 'local' });AxMCPStdioTransport owns its child process. After local.close(), also call
stdio.terminate() until the stdio transport exposes the common close()
lifecycle directly.
Attach clients in constructor or forward options. Per-call options override instance defaults.
const gen = ax('question:string -> answer:string', { mcp: docs });
const result = await gen.forward(llm, { question }, {
mcpContext: [
{ client: 'docs', resource: { uri: 'docs://guide' } },
],
});mcpContext resolves selected prompts or resources before the first model
call and adds attributed, untrusted context. Native tool calls retain client
identity and raw MCP results in memory. streamingForward() keeps Ax output
streaming separate from MCP progress and task events.
const assistant = agent('query:string -> answer:string', {
mcp: [docs, search],
mcpInheritance: 'all',
functionDiscovery: true,
contextFields: [],
});Agents expose native modules under mcp.<namespace>:
mcp.docs.tools.<tool>
mcp.docs.prompts.list()
mcp.docs.prompts.get(name, args)
mcp.docs.resources.list()
mcp.docs.resources.templates()
mcp.docs.resources.read(uri)
mcp.docs.resources.subscribe(uri)
mcp.docs.resources.unsubscribe(uri)
mcp.docs.tasks.get(taskId)
mcp.docs.tasks.cancel(taskId)
mcp.docs.complete(...)Modern modules also expose task input/update behavior through the client.
tasks.list() and tasks.result() are legacy task-draft compatibility APIs and
reject modern servers.
Use mcpInheritance: 'all', 'none', or a namespace allowlist. The resulting
live execution context propagates through Agent stages, llmQuery, RLM, and
child programs. Large catalogs participate in Agent discovery; do not copy
their tools into an inline functions array.
Pass mcp in Flow defaults or forward options. Nested nodes inherit the same
execution context unless mcpInheritance restricts it. Parallel nodes share
the client while respecting its concurrency limit and abort signal.
Use axMCPChat(ai, request, { mcp }) for a high-level non-streaming native MCP
tool loop. Do not build a second ad-hoc tool dispatcher around ai.chat().
An endpoint is only the server address. The server owns tool names, prompt names, resource names, resource URIs, and URI templates. Discover one cloned snapshot before asking users to configure identifiers:
const catalog = await docs.inspectCatalog();
console.log(catalog.tools);
console.log(catalog.prompts);
console.log(catalog.resources);
console.log(catalog.resourceTemplates);
const prompt = await docs.getPrompt('review', { topic: 'MCP' });
const resource = await docs.readResource('docs://guide');
const completion = await docs.complete(reference, argument);inspectCatalog({ refresh: true }) forces fresh bounded pagination. Snapshot
mutation cannot change the live client. List-change notifications refresh the
catalog revision, and native Ax model steps rebuild tool definitions when that
revision changes. Concrete resources can be selected immediately. URI
templates are discoverable but never expanded automatically; applications
construct an authorized concrete URI and may use MCP completion to suggest
argument values.
Modern tool calls, prompt reads, and resource reads may return
resultType: 'input_required'. Ax fulfills the embedded roots, sampling, or
elicitation requests through the same host handlers used by legacy server
requests, then repeats the original operation with the latest input responses
and byte-exact requestState.
Configure only handlers the host can enforce. Ax limits the loop to five input
rounds by default; use maxInputRounds for a stricter policy. A missing handler
or exhausted round limit is a protocol error. The tool concurrency slot stays
held throughout all rounds.
Modern Tasks v2 are server-directed. callTool() auto-awaits an unsolicited
task by default, so Ax tool bindings keep returning the final tool result. Use
callToolOutcome() or taskHandling: 'expose' when the application needs the
task handle, and answer input_required work with provideTaskInput():
const outcome = await docs.callToolOutcome('reindex', { scope: 'all' });
if (outcome.kind === 'task') {
const task = await docs.getTask(outcome.task.taskId);
if (task.status === 'input_required') {
await docs.provideTaskInput(task.taskId, {
approval: { action: 'accept', content: { approved: true } },
});
}
if (task.status === 'working') await docs.cancelTask(task.taskId);
}Use subscribeTaskStatus or subscribeEvents for observation. Keep polling
available because task notifications are optional. Pass Ax abort signals
through program execution; never blindly replay a tool call after an uncertain
post-side-effect failure.
callToolTask(), listTasks(), and getTaskResult() remain functional only
for legacy task-draft servers and are deprecated. Modern completed results and
errors are embedded in tasks/get.
Use AxMCPEventSource with AxEventRuntime. A subscription callback only
publishes an event into the inbox. Explicit routes decide whether to observe,
invalidate, wake, or resume.
const source = new AxMCPEventSource({
client: docs,
resourceSubscriptions: {
select: (resource) =>
resource.mimeType === 'text/markdown' &&
resource.name === 'Engineering guide',
},
identity: { tenantId: 'tenant-1' },
trust: 'authenticated',
});
const runtime = eventRuntime({
allowVolatile: true,
sources: [source],
routes: [
...axMCPEventRoutes({ client: docs }),
eventRoute('guide-updated')
.types('mcp.resource.updated')
.authenticated()
.wake(
eventTarget('reviewer')
.program(reviewer)
.ai(llm)
.input((input) =>
input.field('uri', eventPath.data('uri'))
)
.build()
)
.build(),
],
});Safe defaults are:
'all' -> explicitly subscribe to all discovered concrete resourcesinvalidateobserveinput_required and terminal task states -> resume the owning continuationThe signature-aware input plan is the data boundary. Raw event data remains in
eventContext; only fields selected with segment-safe eventPath descriptors
become program inputs. Use multiple matching routes to fan one notification out
to multiple Agents with independent authorization and run records.
Managed sources refresh and diff their selection after
notifications/resources/list_changed. They keep the prior selection if a
selector throws, retain successful wire transitions after a partial failure,
and retry incomplete work on the next change or reconnect. The client tracks a
separate logical owner for manual subscriptions, every source, and restored
intent: only the first owner sends resources/subscribe, and only the last
release sends resources/unsubscribe. Closing a source cannot break another
owner. Closing the client terminates all ownership and transport state.
Listening is era-aware. Legacy clients maintain resource subscriptions with
resources/subscribe and resume a GET/SSE stream with Last-Event-ID when the
server supports it. Modern clients place catalog interests, concrete resource
URIs, and known task IDs in subscriptions/listen; changes restart that POST
stream with a fresh request ID and no resume header. In both eras,
startListening() is nonblocking and returns a handle with ready, done,
and close().
For the detailed lifecycle and troubleshooting guide, read
docs/MCP_SUBSCRIPTIONS.md and use the checked-in six-language MCP examples.
Configure handlers on AxMCPClient when advertising the corresponding client
capability:
sampling for sampling/createMessageelicitation for form or URL elicitationroots for roots/listonProgress, onLoggingMessage, and onTaskStatus for observationDo not advertise a client capability without a working host handler and policy.
For simple authentication, compose axMCPBearerAuthentication,
axMCPBasicAuthentication, axMCPAPIKeyAuthentication,
axMCPHMACAuthentication, or a caller-defined strategy in the HTTP transport.
Use the transport oauth option for protected-resource discovery, PKCE,
client metadata or dynamic registration, refresh, challenge-driven scope
step-up, DPoP, PAR/JAR/RAR, mTLS, revocation, introspection, client credentials,
or enterprise-managed authorization. Supply persistent token and registration
stores in distributed deployments. Never serialize tokens into Ax program or
event state.
Keep SSRF protection enabled for remote discovery and redirect handling. Relax loopback or HTTP restrictions only for controlled local development.
For checked-in Streamable HTTP examples, set AX_MCP_ENDPOINT. A localhost
TypeScript demo must opt in explicitly with
ssrfProtection: { allowHTTP: true, allowLoopback: true }; never copy that
configuration to a remote endpoint. Generated examples use their equivalent
requireHttps / allowLocalhost / allowPrivateNetworks fields only for
127.0.0.1.
For a real local check, run src/examples/mcp-event-demo-server.ts, set
AX_MCP_ENDPOINT=http://127.0.0.1:3001/mcp, and trigger
/control/resource or /control/task/complete. The mandatory credential-free
matrix is npm run test:mcp-events:generated; provider-backed examples are
advisory and require their documented API key.
Negotiate official extensions through client capabilities. Use
AxMCPAppBridge for MCP App resources and host messages; enforce CSP,
permissions, visibility, allowed tools, and untrusted model-context policy.
Do not render arbitrary HTML returned from a normal tool result as an MCP App.
Wrap a real transport with AxMCPRecordingTransport to capture deterministic
protocol interactions. Use AxMCPReplayTransport for tests, optimization, and
evaluation. Live MCP evaluation is rejected by default because repeated model
runs could repeat external side effects; opt in only deliberately.
finally.Generated transports currently supervise legacy long-lived GET/SSE connections
and resume with Last-Event-ID when available. Generated event runtimes remain
host-driven: schedule delayed work with nextDueAt() and runDue(). Rust hosts
also call AxMCPEventSource.poll() to drain protocol callbacks on the host
thread. Close the source/runtime before the caller-owned client so unsubscribe
and cancellation messages can still be sent.
For generic inbox, continuation, store, and sink behavior, use the
ax-event-runtime skill. For program-specific behavior, combine this skill with
ax-gen, ax-agent, or ax-flow.
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.