Use fentaris(...) as the application boundary. Put upstream servers, identity, policy, logging, endpoint settings, and runtime hooks in one config-driven proxy.
Quick Start
import { fentaris, group, mcp, policy, stdio, user } from "@fentaris/core";
const supportPolicy = policy("support")
.mcp("github")
.allow("create_issue")
.mcp("docs")
.allow("search");
const proxy = fentaris({
servers: [
mcp("github", {
displayName: "GitHub",
transport: stdio({ command: "github-mcp-server" }),
}),
mcp("docs", {
displayName: "Docs",
transport: stdio({ command: "node", args: ["./dist/docs-server.js"] }),
}),
],
groups: [
group({
id: "support",
users: [user("alice")],
policy: supportPolicy,
}),
],
port: Number(process.env.PORT ?? 3000),
host: process.env.FENTARIS_HOST ?? "127.0.0.1",
path: "/mcp",
name: "fentaris-proxy",
version: "0.1.0",
autoLog: true,
});
await proxy.start();
Choose a Stable Endpoint
Client integrations usually store the MCP endpoint. Keep path stable even if ports or hosts change.
const proxy = fentaris({
servers,
port: Number(process.env.PORT ?? 3000),
host: "127.0.0.1",
path: "/mcp",
});
If you deploy behind a reverse proxy, keep the public URL stable and move only the internal service address. The default listener binds to 127.0.0.1; use host: "0.0.0.0" only when the service is intentionally exposed by the deployment boundary.
const proxy = fentaris({
servers,
name: "fentaris-proxy",
version: "0.1.0",
port: 3000,
host: "0.0.0.0",
path: "/mcp",
});
name and version are surfaced to MCP clients during initialization. Treat them as control-plane identity for logs and client diagnostics.
Server names become tool prefixes, so they should remain stable across environments.
mcp("analytics", {
displayName: "Analytics",
transport: stdio({ command: "node", args: ["./dist/analytics-server.js"] }),
})
Use the same server names in staging and production. Names such as analytics-staging change tool prefixes and can break clients.
Add Request Identity Early
Resolve user, tenant, and trace metadata at the proxy edge.
const proxy = fentaris({
servers,
user: (req) => ({
id: req.headers["x-user-id"] as string | undefined,
tenantId: req.headers["x-tenant-id"],
traceId: req.headers["x-trace-id"],
environment: req.headers["x-env"],
}),
});
This makes audit, logging, policy, and upstream env injection use the same request context.
Enforce Policy at the Edge
Use policy(...), group(...), and user(...) for durable authorization.
const adminPolicy = policy("admin-full-access").mcp("*").allow("*");
const proxy = fentaris({
servers,
groups: [
group({
id: "admins",
users: [user("admin")],
policy: adminPolicy,
}),
],
});
Use middleware for request-time validation that depends on arguments, environment, or runtime state.
proxy.mcp("github").tool("create_issue", async (ctx, next) => {
if (typeof ctx.args?.title !== "string") {
return ctx.deny("Issue title is required.");
}
return next();
});
Register Servers After Construction
You can declare policies in fentaris(...) and attach upstream MCP servers with app.mcp(...) before start(). Fentaris validates the final server visibility when the proxy starts.
import { fentaris, group, policy, streamableHttp, user } from "@fentaris/core";
const app = fentaris({
groups: [
group({
id: "limited",
users: [user("guest")],
policy: policy("limited").mcp("specification").allow("*"),
}),
],
});
app.mcp("specification", {
transport: streamableHttp({
url: "https://mcp.specification.website/mcp",
}),
});
If a policy references a server that is still missing at app.start(), Fentaris fails startup with FENTARIS_CONFIG_POLICY_SERVER_NOT_VISIBLE.
Compose Governance After Construction
Use app.policy(...), app.group(...), and app.mcp(...) together when modules need to contribute to one proxy instance.
import { fentaris, stdio, user } from "@fentaris/core";
const app = fentaris({
port: Number(process.env.PORT ?? 3000),
path: "/mcp",
});
app.policy("readonly")
.mcp("github")
.allow("search_issues");
app.group("guests")
.users(user("guest"))
.policy("readonly");
app.mcp("github", {
displayName: "GitHub",
transport: stdio({ command: "github-mcp-server" }),
});
await app.start();
Repeated app.policy("readonly") calls return the same named policy, so modules can add permissions to a shared declaration before the proxy starts.
Filter tool lists before exposing them to clients.
proxy.on("tools:list:after", ({ ctx, tools }) => {
if (ctx.user.environment !== "prod") {
return tools;
}
return tools?.filter((tool) => !tool.name.includes("__experimental"));
});
Keep visibility logic in one place. It makes production and staging tool lists easier to audit.
Proxy Resources, Prompts, and Completions
Fentaris proxies MCP resources, resource templates, prompts, and argument completion when at least one upstream transport supports those methods.
Prompts use the same namespace format as tools:
Resources and resource templates use Fentaris-owned URIs:
fentaris://resources/github/file%3A%2F%2F%2Fissues%2F123.md
fentaris://resource-templates/github/file%3A%2F%2F%2Fissues%2F%7Bid%7D.md
Clients should pass these values back unchanged to resources/read, prompts/get, and completion/complete.
Add Local Capabilities
Use app.local(name) when the Fentaris app itself should expose MCP tools, resources, prompts, or completions beside upstream MCP servers.
import { fentaris, mcp, policy, stdio } from "@fentaris/core";
const app = fentaris({
policy: policy("dev")
.mcp("github")
.allow("search_issues")
.mcp("workspace")
.allow("status")
.mcp("workspace")
.allowCapability({ operation: "resource:read", target: "config://current", targetKind: "resource" }),
servers: [
mcp("github", {
transport: stdio({ command: "github-mcp-server" }),
}),
],
});
app.local("workspace")
.tool("status", { inputSchema: { type: "object" } }, async (ctx) => ({
content: [{ type: "text", text: `subject=${ctx.subject?.id ?? "anonymous"}` }],
}))
.resource("config://current", { name: "Current config" }, async () => ({
contents: [{ uri: "config://current", text: "{}" }],
}))
.prompt("review_pr", { arguments: [{ name: "diff" }] }, async (_ctx, params) => ({
messages: [{ role: "user", content: { type: "text", text: String(params.arguments?.diff ?? "") } }],
}));
Local tools and prompts use the same public naming convention as upstream servers, such as workspace__status and workspace__review_pr. Local resources and resource templates use the same Fentaris proxy URI helpers with workspace as the server name.
A local namespace name cannot match an upstream MCP server name. Fentaris reports a configuration diagnostic before serving requests.
Customize Request Logging
proxy.use(async (ctx, next) => {
ctx.log.setTag("subject", ctx.subject?.id ?? "anonymous");
ctx.log.info("proxy.request");
return next();
});
Use a stable tag such as traceId, user.id, or tenantId so one tool call can be traced across systems.
Close Cleanly
When the proxy shuts down, close upstream transports to avoid orphaned server processes.
process.on("SIGTERM", async () => {
await proxy.close();
process.exit(0);
});
Handle SIGINT in local development and SIGTERM in production.
Low-Level API
new McpProxy(...), new McpServer(...), and explicit transport constructors remain available for advanced integrations and compatibility. New applications should start with fentaris(...), mcp(...), and transport helpers.
Operational Checklist
- Keep server names stable across environments.
- Keep
path stable for client integrations.
- Resolve user, tenant, and trace metadata at the edge.
- Prefer
policy(...) for durable authorization.
- Use middleware for argument-aware runtime checks.
- Avoid hard-coding secrets; inject them through credential or env configuration.
- Close the proxy on process shutdown.