Skip to content

McpServer

The main class for building MCP servers with frac.

import { McpServer } from "@usefractal/frac/server";
const server = new McpServer(
serverInfo: { name: string; version: string },
options: McpServerOptions
);
ParameterTypeDescription
serverInfo.namestringName of your MCP server
serverInfo.versionstringVersion of your server
optionsMcpServerOptionsConfiguration options
type McpServerOptions = {
// Additional options as needed
};

The underlying Express app. Use this to register custom routes, middleware, or settings on the HTTP server backing your MCP server.

import { McpServer } from "@usefractal/frac/server";
const server = new McpServer({ name: "my-app", version: "1.0" });
server.express.get("/health", (_req, res) => {
res.json({ status: "ok" });
});
server.express.set("trust proxy", 1);

express.json() is pre-applied. Register your handlers before run(); after run(), frac appends dev-mode middleware (in development), the /mcp route, and the default error handler in that order.

Register a tool. Provide view to bind the tool to a view. See registerTool for the full API.

server.registerTool(config, handler);

Register Express middleware on the underlying HTTP server. Supports optional path filtering.

// Global middleware
server.use(cors());
// Path-scoped middleware
server.use("/api", authMiddleware);

Register Express error handler on the underlying HTTP server. Supports optional path filtering.

// Global error handler
server.useOnError((err, req, res, next) => {
console.error(err);
res.status(500).send('Something broke!');
})
// Path-scoped error handler
server.useOnError('/api', (err, req, res, next) => {
console.error(err);
res.status(500).send('Something broke!');
})

There is a default error handler already registered, defined as:

function defaultErrorHandler(
err: unknown,
_req: express.Request,
res: express.Response,
_next: express.NextFunction,
) {
console.error("Error handling MCP request:", err);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}

Register MCP protocol-level middleware using an onion model. Middleware wraps request/notification handlers and can inspect, modify, or short-circuit them.

Must be registered before calling server.run() or server.connect().

server.mcpMiddleware(handler);
server.mcpMiddleware(filter, handler);
ParameterTypeDescription
filterMcpMiddlewareFilterOptional. Determines which methods the middleware applies to. Omit for catch-all.
handlerMcpMiddlewareFnThe middleware function. Call next() to continue the chain.
PatternExampleMatches
Exact method"tools/call"Only tools/call
Wildcard"tools/*"Any method starting with tools/
Category"request"All requests (with extra context)
Category"notification"All notifications (extra is undefined)
Array["tools/call", "resources/read"]Multiple patterns (OR logic)
type McpMiddlewareFn = (
request: { method: string; params: Record<string, unknown> },
extra: McpExtra | undefined,
next: () => Promise<unknown>,
) => Promise<unknown> | unknown;
  • request — the incoming MCP request with method and params
  • extra — SDK context (McpExtra) for requests, undefined for notifications
  • next() — invoke the next middleware or original handler. Can only be called once per middleware.

When using an exact method string, both params and extra are narrowed automatically:

server.mcpMiddleware("tools/call", (request, extra, next) => {
// request.params is typed as CallToolRequest["params"]
// extra is typed as McpExtra (not undefined)
console.log(`Tool called: ${request.params.name}`);
return next();
});

Logging all requests:

server.mcpMiddleware("request", (request, extra, next) => {
console.log(`[MCP] ${request.method}`, request.params);
return next();
});

Auth guard on tool calls:

server.mcpMiddleware("tools/call", async (request, extra, next) => {
const token = extra.requestInfo?.headers?.["authorization"];
if (!token) {
throw new Error("Unauthorized");
}
return next();
});

Modifying params:

server.mcpMiddleware("tools/call", (request, extra, next) => {
request.params = { ...request.params, injectedAt: Date.now() };
return next();
});

Multiple middleware (onion order):

const server = new McpServer({ name: "my-app", version: "1.0" }, {})
.mcpMiddleware((request, extra, next) => {
console.log("outer: before");
const result = await next();
console.log("outer: after");
return result;
})
.mcpMiddleware((request, extra, next) => {
console.log("inner: before");
return next();
});
// Logs: outer: before → inner: before → handler → outer: after

Export the server type for client-side type inference:

const server = new McpServer({ name: "my-app", version: "1.0" }, {})
.registerTool(
{
name: "search",
inputSchema: { query: z.string() },
view: { component: "search" },
},
async ({ query }) => {
return { structuredContent: { results: [] } };
},
);
// Export for generateHelpers
export type AppType = typeof server;

Middleware that requires a valid Bearer token on every request. Use this when every tool needs sign-in.

import { requireBearerAuth } from "@usefractal/frac/server";
server.use(
"/mcp",
requireBearerAuth({ verifier: { verifyAccessToken } }),
);
  • Valid Bearer token: the request goes through, authInfo can be accessed in handler extra argument.
  • Missing / invalid / expired tokens: returns a 401 (with WWW-Authenticate), insufficient scopes get a 403.

Middleware that validates a Bearer token when present, and lets requests through unauthenticated when no Authorization header is sent. Pair with per-tool securitySchemes for mixed-auth servers.

import { optionalBearerAuth } from "@usefractal/frac/server";
server.use(
"/mcp",
optionalBearerAuth({
verifier: { verifyAccessToken },
}),
);

Behavior:

  • No Authorization header: the request goes through and the downstream handler decides whether to reject based on its own securitySchemes.
  • Valid Bearer token: the request goes through, authInfo can be accessed in handler extra argument.
  • Missing / invalid / expired tokens: returns a 401 (with WWW-Authenticate), insufficient scopes get a 403.

Takes the same Options as requireBearerAuth.

Both helpers accept the same BearerAuthMiddlewareOptions:

type BearerAuthMiddlewareOptions = {
verifier: OAuthTokenVerifier;
requiredScopes?: string[];
resourceMetadataUrl?: string;
};
FieldTypeRequiredNotes
verifierOAuthTokenVerifierYesThe provider-specific token check. See Verifier for the contract.
requiredScopesstring[]NoServer-wide scope floor: every accepted token must include all listed scopes, or the request is rejected with 403 InsufficientScopeError. Per-tool securitySchemes enforcement layers on top of this. For optionalBearerAuth, this is only checked when a token is actually sent.
resourceMetadataUrlstringNoAbsolute URL to your OAuth 2.0 Protected Resource Metadata document. When set, it’s appended to the WWW-Authenticate header on 401 responses so clients can discover the authorization server programmatically.

Both requireBearerAuth and optionalBearerAuth accept a verifier whose only required method is verifyAccessToken(token: string): Promise<AuthInfo>. This is the provider-specific piece you have to write.

Contract:

  • Resolve with an AuthInfo describing the validated token. The middleware sets it so tool handlers receive it in extra.authInfo.
  • Throw InvalidTokenError to reject the token because of malformed, bad signature, expired, or any failed claim check (issuer, audience, etc.). The middleware returns a 401 with the right WWW-Authenticate header.

You don’t need to check scopes in the verifier: the middleware enforces requiredScopes against authInfo.scopes automatically and returns 403 if a scope is missing.

Required fields on AuthInfo:

FieldTypeNotes
tokenstringThe raw bearer token
clientIdstringOAuth client_id (often the azp or client_id claim).
scopesstring[]Scopes granted to the token. Checked by middleware requiredScopes and by per-tool securitySchemes checks.
expiresAtnumberUnix seconds. Required: requireBearerAuth rejects tokens with no expiration.
extraRecord<string, unknown>Optional: anything else you want available in handlers (e.g. sub, email).
import { type AuthInfo, InvalidTokenError } from "@usefractal/frac/server";
async function verifyAccessToken(token: string): Promise<AuthInfo> {
try {
// Validate with your provider (JWT verification, introspection, etc.).
const payload = await validateToken(token);
return {
token,
clientId: payload.client_id,
scopes: payload.scope.split(" "),
expiresAt: payload.exp,
extra: { sub: payload.sub },
};
} catch (err) {
throw new InvalidTokenError(
err instanceof Error ? err.message : "Token validation failed",
);
}
}

Express router that serves the OAuth 2.0 Protected Resource Metadata document at /.well-known/oauth-protected-resource, so clients can discover the authorization server programmatically.

Example:

import { mcpAuthMetadataRouter } from "@usefractal/frac/server";
server.use(
mcpAuthMetadataRouter({
oauthMetadata: {
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
response_types_supported: ["code"],
},
resourceServerUrl: new URL("https://api.example.com/mcp"),
scopesSupported: ["search.read"],
}),
);

The following middleware types are available from @usefractal/frac/server:

import type {
AuthInfo,
AuthMetadataOptions,
BearerAuthMiddlewareOptions,
McpExtra,
McpMiddlewareFn,
McpMiddlewareFilter,
McpTypedMiddlewareFn,
McpMethodString,
SecurityScheme,
} from "@usefractal/frac/server";