Skip to main content
One server, many agents

One Server, Many Agents

Turn your MCP server into a shared, multi-app service—without transport gymnastics TL;DR: The MCP spec and official SDKs give you transports and sessions, but most examples wire a single connection to a single server instance. That’s fine until you want several IDEs, bots, and dashboards to share one server—and to host multiple apps behind clean auth and scopes. FrontMCP makes that the default with sessions, transport identity, per-app scoping, DI, decorators, and authentication out of the box.
This is not a “stdio vs SSE” post. The issue is the connection model and defaults most examples encourage—regardless of transport. Streamable HTTP + sessions is the spec-aligned path for multi-client servers; FrontMCP productizes it and layers scoping, auth, and DI on top.

How the “one client, one server” pattern sneaks into production

If you follow the happy path:
  1. You open the official @modelcontextprotocol/sdk docs.
  2. You copy the example that spins up a server.
  3. You attach a transport (stdio, HTTP, SSE, or Streamable HTTP).
  4. You connect an MCP client, see your tools, call them, and ship a demo.
So far, life is good:
  • One IDE ↔ one MCP client ↔ one server.
  • Logs are readable.
  • JSON-RPC messages look clean.
  • You can restart everything with a single command.
The problems show up the moment you try to do something slightly more realistic:
  • A second IDE instance for another developer.
  • A background worker using the same tools.
  • A dashboard that introspects tools and runs them on behalf of users.
  • Maybe even multiple agents orchestrating workflows together.
You quickly notice patterns like:
  • N agents = N server processes Each agent spins its own server, often as a child process or a separate container.
  • Zombie servers When a client crashes, the server it launched may keep running.
  • Scattered logs and state Caches, warmups, and logs live in different processes.
  • Awkward auth stories Access tokens and OAuth flows are tied to a single client/server pair.
None of these are protocol problems. They’re shape-of-server problems:
You accidentally treated your MCP server as a client-owned subprocess instead of a shared, long-lived service.

Why this isn’t really about stdio vs HTTP vs SSE

It’s tempting to blame the transport:
  • “Maybe stdio is the problem.”
  • “Maybe SSE is the problem.”
  • “Maybe I should just switch to Streamable HTTP and everything will be fine.”
Transport matters, but the deeper issue is independent of the wire:
  • If your code assumes one transport = one connection = one client,
  • Then it doesn’t matter whether that transport is stdio, SSE, or Streamable HTTP,
  • You’ll still end up multiplying servers for every serious client.
The MCP spec’s Streamable HTTP transport is explicitly designed to support multiple clients + sessions. The challenge is that most starter servers don’t implement a multi-client server model; they implement a single-client demo. That’s where you start writing glue:
  • a homegrown session store,
  • hand-managed transport IDs,
  • per-app routing,
  • DIY auth integration,
  • and a bunch of conditionals sprinkled across tools and handlers.
You can do it—but you’re building a framework on evenings and weekends.

The real problem you’re hitting

When you move beyond a single client, you suddenly need to solve:
  • Session IDs & transport identity So requests don’t collide across clients and nodes, and you can safely resume or route streams.
  • Per-app scoping So multiple products/tenants can share one host without stepping on each other.
  • Auth surfaces Remote IdP or local issuer, wired consistently and scoped correctly to apps and tools.
  • Lifecycle management Sessions that can be created, resumed, and ended in a way that survives proxies, restarts, and load balancers.
  • DI & hooks So you can inject config, secrets, caches, tenants, and logging without duplicating boilerplate in every tool.
All of that is infrastructure work. Necessary, but not what makes your product interesting. What you actually want is:
  • “Here are my tools and apps, written like normal TypeScript services.”
  • “Give me a multi-agent, multi-app MCP server that Just Works.”

What we actually want from an MCP server

If you strip away the noise, most teams want their MCP server to behave like a modern web backend:
  1. Multi-client by design
    • Many IDEs, agents, and services share one server.
    • Sessions keep each client isolated and resumable.
    • Transport identity is stable across reconnections and nodes.
  2. Multi-app composition
    • Group tools into apps by product/domain/tenant.
    • Sometimes expose a unified toolbox.
    • Sometimes isolate apps with separate auth and base paths.
    • Sometimes expose an app as a standalone MCP server.
  3. Auth-aware tool surface
    • The tools a client sees are based on who is logged in, not just what’s deployed.
    • OAuth scopes and identities matter.
  4. Typed code with DI
    • Tools look like clean TypeScript handlers.
    • Inputs/outputs are validated with Zod.
    • Dependencies (DB, caches, external APIs) are injected via DI.
  5. Adapters for existing systems
    • Already have REST APIs? Use OpenAPI, not Ctrl+C/Ctrl+V into new @Tool classes.
The MCP spec gives you the foundations (JSON-RPC, transports, sessions). FrontMCP takes those foundations and bakes them into a framework that assumes multi-client, multi-app from day one.

What FrontMCP flips to “on by default”

FrontMCP is a TypeScript-first framework that treats MCP servers like proper services, not disposable processes.

Multi-client server

Start a Streamable HTTP host that accepts many clients concurrently. Sessions and transport IDs are handled for you, so multiple IDEs, copilots, and workers can share one server safely.

Multi-app composition

Host several apps under one server or isolate them via splitByApp. Each app gets clean base paths and can define its own auth.

Auth that fits

Use remote OAuth to front an external IdP, or local OAuth with a built-in issuer. Scope auth at the server or per app.

DI + decorators

Describe servers, apps, tools, resources, and prompts with decorators. Inject providers with GLOBAL, SESSION, or REQUEST scope. Add hooks for logging, rates, and policy without boilerplate.

Standard MCP vs FrontMCP (visually)

Sometimes it’s easier to just see it: Before vs. after Before vs. after
  • On the left: the “one client, one server” pattern duplicated per agent.
  • On the right: one FrontMCP server hosting many apps and many agents in a spec-aligned way.

Show me the code (multi-agent, multi-app, scoped & authenticated)

Let’s look at what a multi-client, multi-app server looks like in FrontMCP. We’ll define:
  • A server with Streamable HTTP.
  • Two apps: billing and docs.
  • Per-app auth for billing.
src/server.ts
import 'reflect-metadata';
import { FrontMcp, LogLevel } from '@frontmcp/sdk';
import BillingApp from './billing.app';
import DocsApp from './docs.app';

@FrontMcp({
  info: { name: 'FrontMCP Demo', version: '1.0.0' },

  // Host multiple apps; split makes base paths & auth per-app:
  splitByApp: true,
  apps: [BillingApp, DocsApp],

  // Streamable HTTP host (multi-client capable)
  http: {
    port: 3001,
    entryPath: '', // align with your .well-known PRM resourcePath, can be /mcp to prevent conflicts
  },

  logging: { level: LogLevel.INFO },
})
export default class Server {}
src/billing.app.ts
import { App } from '@frontmcp/sdk';
import CreateInvoice from './tools/create-invoice.tool';

@App({
  id: 'billing',
  name: 'Billing',
  tools: [CreateInvoice],
  // Per-app auth surface:
  auth: {
    type: 'remote',
    name: 'corp-idp',
    baseUrl: 'https://auth.example.com',
  },
})
export default class BillingApp {}
src/tools/create-invoice.tool.ts
import { Tool, ToolContext } from '@frontmcp/sdk';
import { z } from 'zod';

@Tool({
  name: 'create_invoice',
  description: 'Create an invoice for a customer',
  inputSchema: { customerId: z.string(), amount: z.number().positive() },
})
export default class CreateInvoice extends ToolContext {
  async execute({ customerId, amount }) {
    // DI-provided services are available from the execution context if needed
    return { status: 'ok', invoiceId: `inv_${customerId}_${Date.now()}`, amount };
  }
}
From here, any number of MCP-capable clients can:
  • Connect over Streamable HTTP.
  • Create their own sessions.
  • Call billing.create_invoice (if authorized).
  • Share the same app code and server instance.
No more “one agent, one server” as an architectural constraint.

Composition modes: one server, many ways to slice apps

The most underrated part of FrontMCP is how you can expose the same apps in different shapes—without changing the app code.

splitByApp: false — unified tool surface per user

When splitByApp is false (the default):
  • All your applications are combined under one MCP server.
  • A connected client can list all tools the logged-in OAuth user is allowed to see, regardless of which app defines them.
  • Auth still applies, but the visible tools and resources are filtered by the user’s identity and scopes.
  • If two apps define tools with the same name, FrontMCP automatically prefixes the tool name with the app id —for example: billing.create_invoice vs ops.create_invoice.
This mode is perfect when you want:
  • A single, large toolbox in the IDE.
  • Agents that can orchestrate across many domains (billing + docs + ops).
  • One “MCP endpoint” for the whole suite, with per-user filtering.
Under the hood, your apps are still cleanly separated. The unified tool surface is a view, not a monolith.

splitByApp: true — per-app isolation

When splitByApp is true:
  • Each app gets its own base path and can define its own auth.
  • You can run multiple products/tenants under one server but with clearly separated surfaces.
  • Clients can connect directly to the specific app they care about, while you still deploy just one server process.
This is ideal when:
  • Apps have distinct consumers and security boundaries.
  • You want a “microservices-like” feel, but still prefer one deployment unit.
  • You’re onboarding teams gradually and want a clear line between their responsibilities.

Standalone apps under /{appId}

Some apps need to serve double duty:
  • They live inside a grouped server (for internal or cross-app usage).
  • They also need to behave as standalone MCP servers for specific clients.
FrontMCP supports apps that are standalone and excluded from the grouped surface:
  • The app is served under the prefix /{appId} as if it were its own MCP server.
  • From the client’s perspective, /{appId} is a standalone MCP endpoint with its own discovery and tools.
  • From your perspective, it’s still the same app class and the same deployment.
This lets you:
  • Expose a clean /{appId} endpoint for partners while keeping internal tools under a unified host.
  • Gradually carve out apps from a single MCP server into more “independent” endpoints without duplicating code.
One deployment, one codebase, multiple MCP “faces” tuned to who’s calling you.

Plugging in existing REST APIs with OpenAPIAdapter

A common reaction to any MCP framework is:
“This is cool, but I already have a REST API. Do I really have to rewrite everything as @Tool classes?”
With FrontMCP, the answer is no.

Connect your REST API with OpenAPIAdapter

The OpenAPI adapter lets you generate tools directly from an OpenAPI spec:
  1. Provide an OpenAPI document (URL or file).
  2. Specify the base URL for requests.
  3. Optionally plug in middleware for auth, tenancy, or logging.
  4. Get a bundle of MCP tools that call your REST endpoints.
Here’s a sketch:
src/openapi.app.ts
import { App } from '@frontmcp/sdk';
import { createOpenApiAdapter } from '@frontmcp/openapi-adapter';

const openApi = await createOpenApiAdapter({
  id: 'my-rest-api',
  name: 'My REST API',
  schemaUrl: 'https://api.example.com/openapi.json',
  baseUrl: 'https://api.example.com',
  requestMiddleware: async (req, ctx) => {
    const token = await ctx.providers.auth.getApiToken();
    req.headers['Authorization'] = `Bearer ${token}`;
    return req;
  },
});

@App({
  id: 'rest',
  name: 'REST-backed tools',
  tools: openApi.tools,
})
export default class RestApp {}
You can find the full details in the docs: 👉 OpenAPI Adapter docs (based on the guide at https://docs.agentfront.dev/0.3/adapters/openapi-adapter). This is the fastest path to:
  • Making your existing API “MCP-native.”
  • Letting agents and IDEs call it via tools.
  • Avoiding a full parallel tool implementation.

Migration playbook (keep your tools; change your host)

If you already have MCP servers or REST APIs, here’s a practical way to move into FrontMCP without a rewrite:
1

Lift your tools into an app

Wrap existing handlers with @Tool or tool()(handler) and group them under @App. Or plug in the OpenAPI adapter to generate tools from your REST API.
2

Choose composition

Start with splitByApp: false for a unified tool surface, then introduce splitByApp: true or standalone apps under /appId where isolation is needed.
3

Wire auth once

Decide on remote vs local OAuth. Make your tool surface depend on the logged-in user and scopes, not just the server process.
4

Expose discovery cleanly

Keep entryPath in sync with your .well-known PRM resourcePath. Test behind your real proxy/load balancer to validate streaming and keep-alives.
5

Turn on observability

Add DI providers and hooks for logging, metrics, and tracing so you can see which apps and tools your agents actually use.
Already running locally? Use frontmcp dev to boot the host and attach multiple MCP clients at once. Multi-agent scenarios stop being hypothetical when you can open two clients and see them share one server.

FAQs

Is this about “legacy SSE vs stdio”? No. The issue is the single-connection default common in examples across transports. Streamable HTTP + sessions already solves multi-client semantics; FrontMCP makes that the default and adds scoping, auth, and DI. Can one server safely host multiple apps for multiple agents? Yes. With splitByApp: false, you get a unified tool surface per user (with automatic name-conflict prefixing). With splitByApp: true, each app gets isolated paths and optional per-app auth, while sharing the same underlying server. Standalone apps under /appId give you separate MCP endpoints when you need them. Can I connect my current REST API without writing tools? Yes. Use the OpenAPIAdapter to generate tools automatically from your existing REST API. Point FrontMCP at your OpenAPI definition and let the adapter create MCP tools for you—no manual tool classes required. See: OpenAPI Adapter docs (based on this guide).

Where to go next