AbsoluteJS

MCP

@absolutejs/mcpv0.28.0betaAI

Remote MCP endpoint for Elysia servers: bring your tools and authorization, the JSON-RPC protocol and OAuth discovery come done correctly.

Serves a remote Model Context Protocol endpoint (streamable HTTP, stateless) from a typed tool, prompt, and resource registry as an Elysia plugin. You supply which tools to expose and how to authorize a request into a caller; the package owns the JSON-RPC protocol, protocol-version negotiation, RFC 9728 discovery metadata, and the 401 challenge that points clients at your authorization server. Nothing in it depends on a model, and the tool shape is structurally compatible with the AIToolMap from @absolutejs/ai, so an existing AI tool registry serves over MCP without conversion.

#Installation

BASH
bun add @absolutejs/mcp

#Capabilities

Overview

MCP tool discovery preserves the OpenID AuthZEN COAZ coaz marker and x-coaz-mapping JSON Schema extension end to end. Use @absolutejs/policy to validate and evaluate the mapping before dispatching an authorized tool call.

Serve a remote Model Context Protocol endpoint — streamable HTTP, stateless — from a tool/prompt/resource registry. You supply which tools to expose and how to authorize a request into a caller; the package owns the JSON-RPC protocol, protocol-version negotiation, RFC 9728 discovery metadata, and the 401 challenge that lets a client find your authorization server. The default negotiated revision is the current finalized 2025-11-25 specification; older finalized revisions remain available when explicitly requested.

Commerce across AI hosts

See Commerce host rules and shared package design for the dated host-policy survey, checkout restrictions, secure handoffs, and reusable AbsoluteJS package boundaries. Host eligibility is implemented; checkout sessions, payment adapters and commerce UI remain planned.

Enforce host commerce eligibility

@absolutejs/mcp/commerce exports evaluateCommerce, typed requirements and review evidence. Tag every commerce tool with commerce; the server hides ineligible tools and checks again before execution (including delayed tasks). Tools without a commerce tag keep their existing behavior.

Show 3 more

resolveReviewedDeployment and createSecureCheckoutHandoff above are consumer integration callbacks, not package exports. Return a CommerceContext based on trusted server configuration, never the tool arguments or a claimed client name. Unknown channels and missing/expired reviews fail closed. A deployment review cannot override the bundled ChatGPT digital-sales, Claude interactive-purchase, or Cursor marketplace paid-access restrictions. Ambiguous profiles intersect.

Reviews must bind the profile, actions, product categories, source URLs and validity window. Capabilities such as external links are checked independently. Classify every possible cart category server-side; never trust a model-supplied product category. meta.commerceDecision records the result for onCall.

This gate is host eligibility only: preserve ownership checks, Agency action authorization, exact purchase confirmation and provider reconciliation. It does not inspect arbitrary text, prompts, resources or untagged tools for sales links. Consumers must use the same evaluator for those surfaces until shared renderers are implemented. An informational-link classification must never redirect users to a transaction. No live sales path is enabled by installing this package.

Agent action enforcement

Tools carrying manifest contract 2 authorization metadata fail closed unless an agency enforcement point is configured. Every call becomes an exact-input action request; allowed calls execute through a short-lived single-use lease and produce a receipt. Requestable denials return an absolute.action_decision payload containing the action id for an approval workflow.

Agency is a required host peer (>=0.7.1 <0.8.0) and is externalized from the MCP build. This guarantees that every transport uses the host's one action ledger instead of embedding a private enforcement runtime. The package tests against exactly 0.7.1; a new Agency minor requires an explicit compatibility release.

Durable Tasks

The package implements native MCP 2025-11-25 task augmentation: execution.taskSupport, client-requested task creation, tasks/get, tasks/result, authorization-bound tasks/list, and terminal-safe tasks/cancel. It also retains the older io.modelcontextprotocol/tasks SEP-2663 wire shape only when an older protocol revision is negotiated.

Clients can use callToolAsTask, then getTask, listTasks, cancelTask, and getTaskResult. Task status never exposes the stored result or authorization key; the final result is returned only by tasks/result with required io.modelcontextprotocol/related-task metadata.

For multi-instance production deployments, use createPostgresMcpTaskStore() and createPostgresMcpSessionStore() after applying mcpPostgresSchemaSql(). Task updates and cancellation protect terminal states in the database, task reads enforce TTL, and session access atomically extends only unexpired sessions. The adapters accept a structural SQL client and do not require a particular PostgreSQL driver.

Show 2 more

Nothing here depends on a model. The tool shape is structurally compatible with @absolutejs/ai's AIToolMap, so an AI tool registry serves over MCP without conversion — but any typed tool registry works.

Peer dependency: elysia.

Define an endpoint

That is a complete member endpoint. GET /mcp returns 405, POST /mcp speaks JSON-RPC, and GET /.well-known/oauth-protected-resource[/mcp] serves the discovery metadata.

Guards, prompts, resources

Everything beyond tools is a hook — the package ships no opinion about billing, storage, or auditing.

The meta scratchpad

Each tools/call gets a fresh meta object shared between tools, beforeCall, and onCall. A tool handler can record what it touched, and your audit hook can read it back:

Feedback: the channel a client can't give you

A connected AI client renders no UI for your server. There is no button for the user to press, so when they say _"that was wrong"_ the only path back to you is the model relaying it. Every MCP server has this hole, and every one of them hand-rolls the same two tools.

FEEDBACK_INSTRUCTIONS is the load-bearing half. Without it a model treats a complaint as something to apologise for, and the signal dies where it was spoken.

Elicitation: ask the user a question mid-call

A tool that can't finish without something only the user knows can ask them (elicitation/create) and wait for the answer.

requestedSchema is a flat object of primitives (string / number / integer / boolean / enum) — the spec restricts it so any client can render a form. The answer is accept (with content), decline (they said no), cancel (they dismissed it), or unsupported (this client can't ask anyone — check canElicit and take another path). Never fabricate an answer for the user; the spec also forbids eliciting sensitive information.

For credentials, third-party OAuth, or payment flows, use mode: "url" with a unique elicitationId and HTTPS URL. The client advertises form and URL modes separately, never prefetches the URL, and returns only the user's consent—not credentials or page contents. Tool handlers can check canElicitUrl before starting the flow. The server rejects non-HTTPS URLs except localhost development URLs and rejects URLs containing embedded credentials.

Show 6 more

The trade-off, stated plainly. Elicitation is the one MCP feature a stateless server cannot do: the question goes out on the SSE stream of an in-flight tools/call, and the client answers on a _separate_ HTTP POST. Two requests have to meet, so the endpoint becomes session-stateful (Mcp-Session-Id). Leave elicitation off — the default — and nothing changes: the server stays stateless, tools/call keeps answering with a plain JSON body, and only tools marked mayElicit ever stream.

Running more than one instance. Behind one server the defaults handle it. Behind several, two different things break, and each has a seam:

Supply neither and run a single instance (or pin sessions). Supply both and elicitation is safe behind a load balancer with no sticky routing — there is a test for exactly that: instance A asks, the answer lands on B, the bus carries it back, and A's call finishes.

AbsoluteJS already ships both production transports. PostgreSQL is the default; Redis is an optional at-most-once fan-out optimization:

The channel is only coordination: durable jobs and side effects belong in @absolutejs/queue / @absolutejs/execution, not Redis pub/sub or NOTIFY.

Consuming a server that elicits? Pass onElicit to createMcpClient — that is what declares the capability, and what the package uses to answer. Omit it and servers are told you cannot ask anyone.

A second, stricter endpoint

mcpServer is per-endpoint, so an admin console is the same call with a different scope, a stricter authorize (role + MFA + a kill switch, re-checked live), a rate-limit beforeCall, and an audit onCall:

Only one endpoint per app should set serveRootMetadata (the un-suffixed alias).

OAuth-native MCP client

createMcpOAuthProvider handles the current MCP authorization flow without coupling to an identity vendor: RFC 9728 protected-resource discovery, OAuth or OIDC authorization-server discovery, Client ID Metadata Document identifiers, PKCE S256, resource indicators, refresh rotation, incremental scope challenges, and optional DPoP proofs. The host owns the user interaction and token store.

The client retries a 401 only once and only after the authorization provider reports success. Metadata fetches require HTTPS, reject redirects, enforce byte limits, verify issuer/resource identity, and use the injected fetch so production deployments can route discovery through @absolutejs/egress.

Secure credit checkout

createCheckoutHandoffTool and createPurchaseStatusTool provide account-bound credit checkout and recovery contracts. The issuer must bind server pricing and identity; route these tools through the commerce guard. Checkout is classified as external_checkout, so restricted and unverified channels cannot discover or execute it. Status works at zero credits. Never pass card data in tool input. See host rules.

createBillingReportTools binds billing status, paginated receipts, and bounded UTC usage reports to authenticated reader callbacks. These non-transactional tools remain available at zero balance. The shared billing projections discard payment/provider secrets and enforce reconciled usage breakdowns. createBillingManagementTool returns a fixed HTTPS browser page requiring normal browser authentication; because that page can initiate purchases, it retains external_checkout commerce classification. Never label a purchase-capable page informational to bypass host restrictions.

Interactive MCP Apps

createBillingApps() adds reusable read-only credit, usage and receipt views with the official browser SDK. Apps capability negotiation, guarded resources and text fallbacks live in the package. See MCP Apps for integration, session migration, CSP restrictions and host validation boundaries.

Saved credit-work results

Use createCreditWorkResult(requestId, work) for initial completion, duplicate requests and read-only recovery. Pass the account-bound saved budget, charged, status and result; extra database fields are not projected. Both MCP content and structuredContent carry the same JSON envelope:

Show 1 more

Read the tool outcome under result; the outer status describes credit work, not action approval or delivery. Saved JSON is decoded once; plain text remains a string, and serialized MCP results remain nested without discarding content. The formatter never retries work. A missing saved result carries a same-ID polling instruction. Bind account identity and authorization before retrieving the saved work; this helper does not authorize access or filter the tool's saved payload.

Budgeted background work

createBackgroundWorkTools({ description, inputSchema, start, read }) exposes start_background_work with a stable requestId and maximum service credits, plus read-only get_background_work recovery. Omit start to support recovery at zero credits or while rollout is disabled. Account ownership must come from the authenticated caller, never tool arguments.

The start adapter must atomically reserve credits, bind immutable work and enqueue a durable job; duplicate IDs must return existing work and reject changed input or budget. The read adapter returns public results and accounting only, without starting, settling or retrying work. createBackgroundWorkResult projects the same progress, results and spend in text and structured content for broad host support. In uncertain states, retain the reservation and do not create a new ID to repeat work.

Start is classified as paid_access; recovery is entitlement_status. Existing commerce host reviews and account/client restrictions still apply. These tools do not enable embedded checkout or override any host's commerce rules.

Show 9 more

Tool audit records

onCall receives normalized result, outcome (completed, failed, or rejected), durationMs, JSON-RPC requestId, unique traceId, and sessionId, alongside the existing caller, arguments, name, success flag, and shared metadata. The trace ID is available in meta.traceId before admission. Rejected guards, unknown tools, malformed calls, and dispatch failures are recorded too. Streaming and deferred task calls produce one final record when their execution finishes. An audit-hook exception is logged without replacing the executed tool result. Hosts must redact credentials and choose payload size and retention limits before persisting records. These records do not include the client's surrounding chat.

Per-account instructions

instructions accepts a string or async ({ caller }) => string | undefined. The callback runs for the authenticated caller at initialization, without a shared caller cache. Refresh policy in tool results when settings can change during an existing session.

Durable audit admission and usage correlation

onCallStart runs before tool dispatch. Persist a start receipt there; throwing prevents execution. onCall runs once at completion, including streamed/deferred execution. Its failure never replaces an executed result: use a durable local outbox or equivalent reconciliation mechanism, not a retry of the effect. A start without completion means incomplete evidence, not success.

For Bun/Node servers, import withMcpCall and currentMcpCall from @absolutejs/mcp/call-context, and set withCallContext: withMcpCall on the server config. The main entry remains browser-compatible; other runtimes can supply their own context adapter. currentMcpCall() exposes server-generated trace identity, request/session identity, tool, server version and the per-call metadata object across asynchronous execution. Use it to correlate downstream metering. Bind domain identifiers only after authorization; incoming arguments and external conversation identifiers are not authority. withMcpCall is available for trusted host adapters. Concurrent calls have isolated contexts. External host conversations, host-side retries and subscription costs are not visible to this server.

Local stdio servers

@absolutejs/mcp/stdio exports serveMcpStdio({ config, caller, scopes, input, write }). Supply an async byte stream (for example Bun.stdin.stream()) and a protocol-only writer. It reuses the HTTP registry and dispatcher with bounded, newline-delimited UTF-8 framing. The launching OS user is the local identity; each tool remains responsible for authorizing remote actions. This initial adapter processes calls sequentially and does not advertise elicitation, Apps, or background tasks. Keep tools bounded; return a job reference for longer work. Never send credentials or private delivery payloads through tool results, stdout, stderr, prompts, or resources. Use a separate trusted local sink for such values.

Stateless streamable HTTP

POST speaks JSON-RPC with plain JSON responses and no session state by default, so the endpoint scales like any other stateless route.

OAuth bearer auth and discovery

verifyBearer runs the standard OAuth access-token checks, and the plugin serves RFC 9728 protected-resource metadata plus the 401 challenge that lets clients discover your authorization server.

Per-call guards and audit

beforeCall can refuse a single call (credits, rate limits) with a message the model relays, onCall audits every call, and a per-request meta scratchpad carries data between tool handlers and hooks.

Mid-call elicitation

Tools marked mayElicit can ask the user a question mid-call and await the answer; opt-in session state plus pluggable store and bus seams make it safe behind a load balancer with no sticky routing.

Built-in feedback tools

feedbackTools and FEEDBACK_INSTRUCTIONS give connected AI clients a channel to report problems and relay user feedback, the signal every MCP server otherwise hand-rolls or loses.

Prompts, resources, and client

Server-side prompt definitions and readable resources are plain hooks, and createMcpClient consumes remote MCP servers, including answering their elicitation requests via onElicit.

Agency action enforcement

Tools with manifest-contract authorization metadata fail closed unless Agency enforcement is configured. Allowed calls bind the exact input to a short-lived single-use execution lease and produce a receipt.

Durable MCP tasks

Native MCP 2025-11-25 tasks support creation, get, result, authorization-bound listing, cancellation, TTL, and terminal-state protection with memory or PostgreSQL stores.

#Protected tool sequence

Protected tools bind authorization to the exact action and consume a short-lived execution lease before effects occur.

  1. Receive
    Validate the MCP request, caller, schema, and declared action.
  2. Authorize
    Ask Agency to approve the canonical action binding.
  3. Fence
    Issue and consume a single-use execution lease.
  4. Execute
    Run the handler and persist its receipt.
  5. Respond
    Return a result or a durable task reference.

#Durable task lifecycle

MCP 2025-11-25 tasks let long-running tools survive transport and process boundaries.

  1. Create
    Create a task with caller ownership, authorization binding, and TTL.
  2. Persist
    Persist task and session state in a durable store.
  3. Observe
    Poll, list, or subscribe without losing ownership checks.
  4. Finish
    Complete, fail, expire, or cancel with a terminal record.

Outcomes

What you can build

Build complete MCP servers

Publish typed MCP tools, prompts, resources, elicitation, feedback, OAuth-native clients, and stricter secondary endpoints from an AbsoluteJS application.

Govern and persist agent work

Route consequential tool calls through Agency decisions and model long-running work as durable tasks instead of keeping requests open indefinitely.

Hardening checklist

Production guidance

Secure the protocol boundaryAuthenticate before tool execution, validate every input, separate endpoint trust levels, persist durable tasks, bound scratchpad data, and retain authorization and execution evidence.

Follow in order

Troubleshooting path

1
A tool call was rejected or stalled
Inspect endpoint discovery, OAuth metadata, guard decisions, parsed tool input, Agency action state, durable task state, and the final MCP result separately.

#Define an endpoint

Partial snippet

Working example for Define an endpoint.

TS
import { Elysia } from "elysia";
import { mcpServer, verifyBearer } from "@absolutejs/mcp";
import { verifyJwt } from "@absolutejs/auth"; // or any JWT verifier

type Caller = { userId: string };

const server = new Elysia().use(
  mcpServer<Caller>({
    path: "/mcp",
    issuer: "https://your.app",
    serverInfo: { name: "your-app", title: "Your App", version: "1.0.0" },
    instructions: "What the model should know about this server.",
    scopesSupported: ["openid", "mcp"],
    serveRootMetadata: true,

    // You decide who is allowed in. verifyBearer does the standard OAuth
    // access-token checks; add your own (billing, role, MFA) on top.
    authorize: async (request) => {
      const token = await verifyBearer({
        audience: "https://your.app/mcp",
        request,
        issuer: "https://your.app",
        requiredScope: "mcp",
        verify: (jwt) => verifyJwt(jwt, publicJwk),
      });
      if ("error" in token) return { ok: false, reason: token.error };
      return { ok: true, caller: { userId: token.subject } };
    },

    // Called once per request; build the tools for this caller.
    tools: ({ caller }) => buildToolsFor(caller.userId),
  }),
);

#Enforce host commerce eligibility

Partial snippet

@absolutejs/mcp/commerce exports evaluateCommerce, typed requirements and review evidence. Tag every commerce tool with commerce; the server hides ineligible tools and checks again before execution (including delayed tasks). Tools without a commerce tag keep their existing behavior.

TS
mcpServer<Caller>({
  // Existing authorize, issuer, path, serverInfo and agency configuration…
  commerce: ({ caller }) => resolveReviewedDeployment(caller),
  tools: () => ({
    open_checkout: {
      commerce: { action: "external_checkout", categories: ["usage_credits"] },
      description: "Open a secure checkout for service credits",
      inputSchema: { type: "object", properties: {} },
      handler: () => createSecureCheckoutHandoff(),
    },
  }),
});

#Agent action enforcement

Partial snippet

Tools carrying manifest contract 2 authorization metadata fail closed unless an agency enforcement point is configured. Every call becomes an exact-input action request; allowed calls execute through a short-lived single-use lease and produce a receipt. Requestable denials return an absolute.action_decision payload containing the action id for an approval workflow.

TS
import { createAgency, createMemoryAgencyStore } from "@absolutejs/agency";

const agency = createAgency({ policy, store: createMemoryAgencyStore() });

mcpServer<Caller>({
  agency: {
    enforcement: agency,
    resolveActor: ({ caller, scopes }) => ({
      agentId: caller.agentId,
      delegationId: caller.delegationId,
      scopes,
      userId: caller.userId,
    }),
  },
  // normal MCP config…
});

#Quick Start

Partial snippet

A complete MCP endpoint: POST /mcp speaks JSON-RPC and the RFC 9728 discovery metadata is served automatically.

TS
import { Elysia } from 'elysia';
import { mcpServer, verifyBearer } from '@absolutejs/mcp';

type Caller = { userId: string };

const server = new Elysia().use(
	mcpServer<Caller>({
		path: '/mcp',
		issuer: 'https://your.app',
		serverInfo: { name: 'your-app', title: 'Your App', version: '1.0.0' },
		instructions: 'What the model should know about this server.',
		scopesSupported: ['openid', 'mcp'],
		serveRootMetadata: true,

		// You decide who is allowed in. verifyBearer does the standard
		// OAuth access-token checks; add your own on top.
		authorize: async (request) => {
			const token = await verifyBearer({
				request,
				issuer: 'https://your.app',
				requiredScope: 'mcp',
				verify: (jwt) => verifyJwt(jwt, publicJwk)
			});
			if ('error' in token) return { ok: false, reason: token.error };
			return { ok: true, caller: { userId: token.subject } };
		},

		// Called once per request; build the tools for this caller.
		tools: ({ caller }) => buildToolsFor(caller.userId)
	})
);

#Guards, Audit, and Feedback

Partial snippet

Guard calls, audit them, and give the connected AI client a feedback channel back to you.

TS
import {
	FEEDBACK_INSTRUCTIONS,
	feedbackTools,
	mcpServer
} from '@absolutejs/mcp';

mcpServer<Caller>({
	// ...as above
	instructions: `${myInstructions} ${FEEDBACK_INSTRUCTIONS}`,

	// Refuse a single call before it runs; the message comes back as
	// an isError tool result the model can relay, not a crash.
	beforeCall: async ({ caller }) =>
		(await outOfCredits(caller))
			? { block: 'Out of credits this cycle.' }
			: undefined,

	// Audit every call. meta carries whatever the tool handler wrote.
	onCall: ({ caller, name, ok, meta }) =>
		recordCall({ caller, name, ok, touched: meta.touched }),

	tools: ({ caller }) => ({
		...myTools(caller),
		...feedbackTools({
			caller,
			store: {
				reportProblem: ({ caller, report }) => file(caller, report),
				submitFeedback: ({ caller, feedback }) => record(caller, feedback)
			}
		})
	})
});

#Governed Durable Tasks

Partial snippet

Enforce consequential tools through the host Agency ledger and persist long-running MCP tasks in PostgreSQL for multi-instance deployments.

TS
import { createAgency, createMemoryAgencyStore } from '@absolutejs/agency';
import { createPostgresMcpTaskStore, mcpServer } from '@absolutejs/mcp';

const agency = createAgency({ policy, store: createMemoryAgencyStore() });

mcpServer<Caller>({
	agency: {
		enforcement: agency,
		resolveActor: ({ caller, scopes }) => ({
			agentId: caller.agentId,
			delegationId: caller.delegationId,
			scopes,
			userId: caller.userId
		})
	},
	tasks: {
		authorizationKey: (caller) => caller.userId,
		shouldCreate: ({ name }) => name === 'long_running_report',
		store: createPostgresMcpTaskStore({ sql }),
		ttlMs: 60 * 60 * 1000
	},
	tools: () => ({
		long_running_report: { taskSupport: 'optional', ...reportTool }
	})
});
Multiple endpoints
mcpServer is per-endpoint: mount a second, stricter instance (different path, scopes, authorize, and guards) for an admin surface. Only one endpoint per app should set serveRootMetadata.
Beta API
The package is pre-1.0; the elicitation and session seams in particular may evolve, so pin an exact version.

Current package surface

What ships today

@absolutejs/mcpv0.28.0 · betaAInpmSource
9entry points

Import surface · click to copy