@aventara/mcp
v1.0.0-rc.12
Published
The MCP server for an Aventara application: its agent layer as Model Context Protocol tools.
Downloads
1,128
Readme
@aventara/mcp
The MCP server of an Aventara application. It turns the configuration's agent layer into
Model Context Protocol tools, so an AI agent (Claude Desktop, Claude Code, any MCP
client) can read and write the application's data under the same validation, exposure and pipelines as every other
caller.
The server lives inside your application. @aventara/nest mounts it at the path the configuration's mcp key names,
over the Streamable HTTP transport, stateless. You install this package beside @aventara/nest; you do not call it
yourself.
npm install --save-exact @aventara/mcp@<your @aventara/core version>It depends on @aventara/core and the official MCP TypeScript SDK (@modelcontextprotocol/sdk).
Turning it on
// src/aventara.config.ts
export async function aventaraConfig(prisma: PrismaService) {
return {
api: "/api",
mcp: "/mcp",
adapter: await createPrismaAdapter({ client: prisma, discovery, provider: "postgresql", driver: "@prisma/adapter-pg" }),
// What the agent may reach: by default exactly what the client may.
agent: {
exposed: true,
exemptions: { resources: { AuditLog: true } },
},
};
}When the configuration has mcp, AventaraModule mounts the endpoint and logs it:
Aventara mounted the MCP endpoint at /mcp (…). With mcp set and this package missing, the application refuses to
start and names the package.
The tools
One tool per operation family the agent layer exposes, and none for a family nothing exposes:
| Tool | Input | Hints |
|---|---|---|
| find | { resource, variant, args? } | read-only |
| create | { resource, variant, args } | not destructive |
| update | { resource, variant, args } | destructive |
| delete | { resource, variant, args } | destructive |
| upsert | { resource, variant, args } | destructive |
| transaction | { steps: [{ resource, family, variant, args? }] } | destructive |
| me | {} | read-only |
resource and variant are enums read from the agent layer, so a resource the agent layer hides is not offered (and is
refused with A2001 if an agent names it anyway). args is the operation's arguments as the protocol defines them
(where, select, data, …). transaction is offered when the agent layer's transactions are interactive. A step can
use a value an earlier step returned with { "$ref": { "operation": 0, "path": ["id"] } }; the server adds the
fingerprints the protocol requires. me is offered when the framework's access provider states its client: it
answers the identity Aventara Access resolves from the request's credentials ({ sub, claims }, or null) as the
envelope's data, holding only what the access section's me schema declares (an identity the schema does not
describe is the server's own failure, A3000). There is no sign-in tool.
Every call runs as the agent origin, through the same decoding and validation as an HTTP call, with the agent
layer's pipelines. A success answers the protocol's envelope ({ data, code, cause }) as structured content and as
text. A failure answers an MCP tool error (isError: true) carrying the envelope, its cause and its issues (path,
code, check, params, message), so the agent can fix its arguments and call again.
The contract resource
The server exposes the Agent Contract as the MCP resource aventara://contract: every resource, its fields and their
types, what can be filtered, selected, created and updated, and the request limits. The tool descriptions and the
server's instructions tell the agent to read it first.
Write safety
What an agent can do is decided where everything else is: exposure (the agent layer, which can only narrow the layer
it extends) and guards. A guard can tell an agent's call by its origin:
const humansOnly: AvGuard = (ctx) => ctx.$origin !== "agent";The tool hints (readOnlyHint, destructiveHint) are hints for the client; they enforce nothing.
Authorization
With mcp.authorization, the endpoint is an OAuth 2.1 protected resource. It never issues tokens: your authorization
server does.
import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(new URL("https://auth.example.com/.well-known/jwks.json"));
mcp: {
path: "/mcp",
authorization: {
servers: ["https://auth.example.com"],
verify: async (token) => {
try {
const { payload } = await jwtVerify(token, jwks, {
issuer: "https://auth.example.com",
audience: "https://api.example.com/mcp",
});
return payload;
} catch {
return null;
}
},
},
},Each server is an https URL. One on your own machine may be http instead, so a local setup needs no TLS:
http://127.0.0.1:4000 or http://[::1]:4000, with any port and path. That is the whole list. Use the address, not
localhost: a name is looked up, and localhost can resolve somewhere else (RFC 8252, section 8.3), so
http://localhost:4000 is refused with the spelling to use, http://127.0.0.1:4000. 0.0.0.0, [::], other
127.x addresses, IPv4-mapped addresses, names such as auth.localhost and URLs with credentials are refused like
any other http URL. Write the URL the way a browser would show it: http://127.1:4000 is refused, and the error gives
the spelling to use. No server URL, https included, may have a query or fragment (an issuer has neither), an empty
path segment (//) or port 0.
An http authorization server is for local development only. Tokens and authorization codes cross it unencrypted, and only clients on the same machine can reach it. Deploy with an https server.
GET /.well-known/oauth-protected-resource/mcpanswers the protected-resource metadata (RFC 9728): the endpoint's URL asresource, yourserversasauthorization_servers, andbearer_methods_supported: ["header"].- A request without
Authorization: Bearer <token>is401withWWW-Authenticate: Bearer resource_metadata="…"naming that metadata, which is how an MCP client discovers where to sign in. As RFC 6750 asks, it carries no error code: the request had no token to be wrong. - Each token goes to
verify.null(orundefined, or a throw) is401witherror="invalid_token". Averifythat throws is also reported to yourdiagnosticssink (anAvMcpDiagnosticholding the error), so an outage of your token check shows in your logs rather than as a run of rejected agents. Anything else lets the request in, and whatverifyreturned is the request'sctx.$state.mcp.claims, beside what the host'sstateoption seeds. Declare its type with the rest of your state:
declare module "@aventara/core" {
interface AvState {
mcp?: { claims: JWTPayload };
}
}The endpoint's URL comes from the request as your platform reports it, so behind a proxy set Express's trust proxy
or Fastify's trustProxy for https and the public host to appear in the metadata.
Without authorization
An endpoint without authorization takes no token, so it answers callers on the same machine only: a loopback peer
address, no proxy forwarding header (Forwarded, X-Forwarded-For, X-Real-IP), a loopback Host (localhost,
127.x.x.x or [::1], any port), and no Origin header but a loopback one. Every other caller gets 403. The Host
rule closes DNS rebinding: a page whose domain is rebound to 127.0.0.1 still sends its own domain as Host. An HTTP/2
caller sends :authority instead of Host, so the open endpoint refuses it too; serve it over HTTP/1.1, or configure
authorization. This is for local development and for an agent running beside the
application. To serve remote agents, configure authorization.
The transport
Streamable HTTP, stateless: every POST is answered on its own with a JSON response, and no session is kept. GET and
DELETE answer 405. A request body is limited to the agent layer's maxRequestBytes.
License
PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
may not use it to build a product that competes with Aventara itself. See LICENSE and
LICENSE-ADDITIONAL-PERMISSION.md.
