Guide to Building an Enterprise Model Context Protocol Server Securely Shared by the Entire Team: Practical Implementation of Streamable HTTP and OAuth 2.1
When we first deployed the team MCP server, we belatedly discovered that an intern could call the trigger_pipeline tool an unlimited number of times. Accessing the server with a single token allowed the use of all tools, and there wasn't even a record of who called which tool and when. While it is easy to set up an MCP server running locally in the Claude desktop app, the story changes the moment 10 team members use it together. Enterprise-level requirements pour in, such as who can access which tools, how credentials are managed, and whether audit logs are being kept.
TL;DR — After reading this, you will be able to build a Streamable HTTP-based MCP server and design an architecture that securely controls team-wide access using the OAuth 2.1 Gateway pattern. Unlike other MCP tutorials, this covers code-level mistakes that lead to actual security incidents, as well as their corresponding response patterns.
In this article, we will take a step-by-step look at building a remote MCP server using the Streamable HTTP transport method, officially adopted in the 2025 MCP specification, and an architecture securely shared by the entire team using OAuth 2.1 authentication. It is assumed that the target audience is backend and full-stack developers with a basic understanding of JWT, OAuth, and REST APIs. Along with TypeScript SDK code examples, it covers security patterns to consider in a real-world production environment and common mistakes.
Key Concepts
What is MCP
The Model Context Protocol (MCP) is an AI integration standard protocol released as open source by Anthropic in November 2024. It defines a common interface used by LLMs to access resources such as file systems, databases, and external APIs. Just as USB-C connects various devices using a single standard, MCP acts as a standard connector between AI agents and external tools.
As of 2025, Anthropic, OpenAI, Google, and Microsoft have all adopted MCP as a standard, and the number of monthly SDK downloads has exceeded 97 million.
Prerequisites for MCP in this article: It is sufficient to know the concepts of Tools (function calls), Resources (data reading), and Prompts (reusable prompt templates). If you are new to MCP, we recommend reading the introduction to the official specification first.
Streamable HTTP: Why SSE Was Abandoned
Existing MCPs used the HTTP+SSE (Server-Sent Events) method. The structure involved the client opening an SSE connection via a GET endpoint and sending a request via a POST endpoint; however, this required managing two endpoints at all times, and maintaining long-term connections in a serverless environment was difficult. Additionally, as the number of SSE connections increased, bottlenecks occurred due to the browser's HTTP/1.1 concurrent connection limit (6 per domain).
Streamable HTTP, added to the MCP specification in March 2025, solves these problems.
| Classification | HTTP+SSE (Old Standard) | Streamable HTTP (New Standard) |
|---|---|---|
| Number of Endpoints | Two (GET + POST) | Single Endpoint |
| Streaming Method | Always SSE | Automatically upgrades to SSE when needed |
| Serverless Support | Limited | On-demand Processing Available |
| Session Management | Complex | Explicit handling with Mcp-Session-Id header |
| HTTP/1.1 connection limit | Requires maintaining a separate GET connection at all times | No limit when SSE is not used |
| Current Status | Deprecated | Officially Recommended Method |
Stateful vs Stateless: Streamable HTTP supports two operating modes. It operates in Stateful mode, which maintains sessions, when the Mcp-Session-Id header is utilized, and in Stateless mode, which processes each request independently without the header. Stateless is cost-effective for serverless deployments.
OAuth 2.1-based MCP Authentication Flow
To share an MCP server among teams, you must control "who can use which tools." Starting with the MCP specification version 2025-11-25 (Authorization specification), the OAuth 2.1-based authentication framework is officially included for remote servers.
OAuth Key Terms Summary
- JWT (JSON Web Token): A signed token. The server can validate the content locally without asking the IdP.
- JWKS(JSON Web Key Set): A collection of public keys released by the IdP. Used for JWT signature verification.
- Authorization Code Flow: This is the flow where the user logs directly into the IdP, receives an authorization code, and exchanges it for a token.
- PKCE(Proof Key for Code Exchange): A security extension that prevents authorization code theft. Required in OAuth 2.1.
The overall authentication flow is as follows.
1. 클라이언트 → MCP 서버: 토큰 없이 요청
2. MCP 서버 → 클라이언트: 401 Unauthorized
+ WWW-Authenticate 헤더 (메타데이터 위치 안내)
3. 클라이언트 → IdP: PKCE 포함 Authorization Code Flow 시작
4. IdP → 클라이언트: 인가 코드 발급
5. 클라이언트 → IdP: 코드 + PKCE verifier로 액세스 토큰 요청
6. IdP → 클라이언트: 단기 액세스 토큰(JWT) 발급
7. 클라이언트 → MCP 서버: Bearer 토큰과 함께 재요청
8. MCP 서버: 토큰 검증
┌─ JWT 방식(권장): JWKS 공개 키로 서명을 로컬 검증 → IdP 추가 요청 없음
└─ Introspection 방식: IdP의 /introspect 엔드포인트에 검증 요청
9. MCP 서버 → 클라이언트: 정상 응답The code examples in this article use the JWT + JWKS method. Since signatures are verified using public keys without requesting an IdP every time a token arrives, latency is low and dependency on the IdP is reduced. The Introspection method is selected when immediate token revocation is required.
PKCE (Proof Key for Code Exchange): This is a security extension that prevents token issuance even if the authorization code is stolen. In OAuth 2.1, PKCE is mandatory for all Authorization Code Flows. The implicit flow of the previous OAuth 2.0 has been completely removed.
Practical Application
1. Implementing a TypeScript Streamable HTTP Server
You can set up a remote MCP server using StreamableHTTPServerTransport in MCP TypeScript SDK v1.10.0 or later.
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { randomUUID } from "crypto";
import { z } from "zod";
const app = express();
app.use(express.json());
// 세션별 transport 인스턴스 관리 (Stateful 모드)
// ⚠️ 주의: 이 Map은 단일 프로세스 메모리에 저장됩니다.
// 서버 재시작 시 모든 세션이 유실됩니다.
// 프로덕션에서는 반드시 Redis 등 외부 저장소로 교체가 필요합니다.
const sessions = new Map<string, StreamableHTTPServerTransport>();
function createMcpServer() {
const server = new McpServer({
name: "enterprise-mcp-server",
version: "1.0.0",
});
server.tool(
"get_jira_issue",
"Jira 이슈 정보를 조회합니다",
{
issueKey: z.string().describe("Jira 이슈 키 (예: PROJ-123)"),
},
async ({ issueKey }) => {
// 실제 구현에서는 Jira REST API 호출
return {
content: [{ type: "text", text: `이슈 ${issueKey}: [API 응답 데이터]` }],
};
}
);
return server;
}
// 단일 엔드포인트로 GET/POST/DELETE 모두 처리
app.all("/mcp", async (req, res) => {
try {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && sessions.has(sessionId)) {
transport = sessions.get(sessionId)!;
} else if (!sessionId && req.method === "POST") {
const newSessionId = randomUUID();
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => newSessionId,
// 스트리밍이 불필요한 요청은 일반 JSON으로 응답해 리소스 절감
enableJsonResponse: true,
});
const server = createMcpServer();
await server.connect(transport);
sessions.set(newSessionId, transport);
// ⚠️ setTimeout은 프로세스 재시작 시 초기화됩니다.
// 슬라이딩 윈도우 방식이 필요하면 Redis TTL을 활용하세요.
setTimeout(() => sessions.delete(newSessionId), 30 * 60 * 1000);
} else {
res.status(400).json({ error: "Invalid session" });
return;
}
await transport.handleRequest(req, res);
} catch (err) {
console.error("MCP 요청 처리 오류:", err);
if (!res.headersSent) {
res.status(500).json({ error: "Internal server error" });
}
}
});
app.listen(3000, () => {
console.log("MCP 서버가 http://localhost:3000/mcp 에서 실행 중입니다");
});| Code Point | Description |
|---|---|
sessions Map |
In-memory session management. Limited to a single process; sessions are lost upon restart. For production, replacement with external storage such as Redis is required. |
app.all("/mcp", ...) |
Handle all HTTP methods with a single endpoint — The core of Streamable HTTP |
enableJsonResponse: true |
Save resources by returning standard JSON when streaming is not needed |
try/catch |
Protect against transport errors halting the entire server |
2. OAuth 2.1 Gateway Middleware Implementation
Instead of embedding OAuth processing code into each individual MCP server, the pattern of placing an MCP Gateway in front is becoming the enterprise standard.
[AI Client: Claude / Cursor]
│
▼
[MCP Gateway / Proxy] ← 토큰 검증, RBAC, 감사 로그, 속도 제한
│
├── JWT 서명 검증 → [JWKS: Okta / Azure AD / Keycloak]
│
▼
┌─────────────────────────────────┐
│ [MCP Server A: Jira Tools] │
│ [MCP Server B: DB Query Tools] │
│ [MCP Server C: CI/CD Tools] │
└─────────────────────────────────┘This is an example of Express middleware that validates tokens at the gateway layer.
import { Request, Response, NextFunction } from "express";
import { createRemoteJWKSet, jwtVerify } from "jose";
// IdP의 JWKS 엔드포인트 — JWT 서명 검증에 필요한 공개 키를 동적으로 가져옵니다.
// 키 교체(rotation) 시 별도 배포 없이 자동 대응됩니다.
const JWKS = createRemoteJWKSet(
new URL("https://your-idp.example.com/.well-known/jwks.json")
);
// 도구별 필요 스코프 매핑 테이블
const TOOL_SCOPE_MAP: Record<string, string[]> = {
get_jira_issue: ["jira.read"],
create_jira_issue: ["jira.write"],
trigger_pipeline: ["cicd.trigger"], // 고위험 작업은 별도 스코프로 격리
query_database: ["db.read"],
};
export async function authMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
// RFC 9728: 인증 실패 시 메타데이터 위치를 클라이언트에게 안내
res.setHeader(
"WWW-Authenticate",
`Bearer realm="mcp", resource_metadata="https://mcp.yourcompany.com/.well-known/oauth-protected-resource"`
);
res.status(401).json({ error: "Authentication required" });
return;
}
try {
const token = authHeader.slice(7);
// JWT 서명을 JWKS 공개 키로 로컬 검증 — IdP에 추가 요청 없음
const { payload } = await jwtVerify(token, JWKS, {
audience: "https://mcp.yourcompany.com",
});
// MCP JSON-RPC 요청에서 도구명 추출
// tools/call 이외의 메서드(initialize, tools/list 등)는 null 반환
const requestedTool = extractToolName(req.body);
if (requestedTool !== null) {
// tools/call 요청: 도구별 스코프 검증
const requiredScopes = TOOL_SCOPE_MAP[requestedTool] ?? [];
const tokenScopes = ((payload.scope as string) ?? "").split(" ");
const hasPermission = requiredScopes.every((s) => tokenScopes.includes(s));
if (!hasPermission) {
await auditLog({
userId: payload.sub,
tool: requestedTool,
result: "DENIED",
requiredScopes,
tokenScopes,
timestamp: new Date(),
});
res.status(403).json({ error: "Insufficient scope", required: requiredScopes });
return;
}
}
// tools/call 이외의 요청(initialize 등)은 토큰 유효성만 확인 후 통과
(req as any).user = payload;
next();
} catch (err) {
res.status(401).json({ error: "Invalid or expired token" });
}
}
function extractToolName(body: any): string | null {
// tools/call 메서드에서만 도구명을 추출합니다.
// initialize, tools/list, resources/list 등 다른 메서드는 null 반환 →
// 호출자(authMiddleware)에서 null 여부를 확인해 스코프 검사를 건너뜁니다.
if (body?.method === "tools/call") {
return body?.params?.name ?? null;
}
return null;
}| Code Point | Description |
|---|---|
createRemoteJWKSet |
Dynamically retrieve IdP's public key for signature verification — Automatically handle key rotation |
TOOL_SCOPE_MAP |
Define required scopes per tool — cicd.trigger Isolate high-risk tools like these into separate scopes |
extractToolName Returns null |
initialize etc. tools/call Requests pass after only checking token validity without scope checks |
WWW-Authenticate Header |
RFC 9728 compliant — Guides clients to automatically discover authentication servers |
auditLog |
Denied access must also be logged — Essential for security audits and anomaly detection |
3. Cloudflare Workers Serverless Deployment
Cloudflare Workers is a good choice for running MCP servers at the edge without the burden of server operation. You can manage session state with Durable Objects and implement SSO with Zero Trust.
// worker.ts
// ⚠️ 아래 코드는 구조를 설명하는 의사 코드입니다.
// Cloudflare Agents SDK와 Vectorize API 시그니처는 변경될 수 있으므로
// 실제 구현 전 Cloudflare 공식 문서를 반드시 확인하세요.
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class MyMcpAgent extends McpAgent {
server = new McpServer({ name: "cf-mcp-server", version: "1.0.0" });
async init() {
this.server.tool(
"search_docs",
"내부 문서를 검색합니다",
{ query: z.string() },
async ({ query }) => {
// Vectorize는 텍스트가 아닌 벡터 값을 직접 전달해야 합니다.
// 실제 구현: AI Workers로 임베딩 생성 후 Vectorize에 질의합니다.
const embedding = await this.env.AI.run(
"@cf/baai/bge-small-en-v1.5",
{ text: query }
);
const results = await this.env.VECTORIZE_INDEX.query(
embedding.data[0],
{ topK: 5 }
);
return {
content: [{ type: "text", text: JSON.stringify(results) }],
};
}
);
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/mcp") {
// Cloudflare Zero Trust가 앞단에서 SSO 처리
// 이미 인증된 요청만 이 핸들러에 도달합니다
return MyMcpAgent.serve("/mcp").fetch(request, env);
}
return new Response("Not found", { status: 404 });
},
};Cloudflare Zero Trust + MCP Server Portals (Open Beta): By applying Cloudflare's access policies in front of MCP servers, you can implement SSO integrated with enterprise IdPs such as Okta, Azure AD, and Google Workspace without separate authentication codes. By resolving the gateway pattern at the platform layer, it serves as an excellent starting point for teams looking to minimize the burden of server operations.
Pros and Cons Analysis
Advantages
| Item | Content |
|---|---|
| Simple Client Implementation | Connect to a single endpoint and automatically utilize streaming when needed |
| Serverless Affinity | Run on demand on AWS Lambda and Cloudflare Workers — Cost-effective |
| Reuse existing HTTP infrastructure | Seamlessly integrate with Load Balancers, CDNs, and API Gateways |
| Proven Security Standards | PKCE Mandatory, Implicit Flows Removed — Reflecting OAuth 2.1 Security Enhancements |
| Enterprise IdP Integration | Reuse existing infrastructure such as Okta, Azure AD, Keycloak, etc. |
| Granular Access Control | Applying the Principle of Least Privilege with Tool-Specific Scopes such as logs.read, incidents.trigger, etc. |
Disadvantages and Precautions
| Item | Content | Response Plan |
|---|---|---|
| Session Management Complexity | Must securely handle the creation, storage, and expiration of Mcp-Session-Id |
Use external storage such as Redis, TTL setting is mandatory |
| HTTP/1.1 Connection Limits | If the number of SSE connections increases, you may hit the browser's concurrent connection limit (6 per domain) | Use HTTP/2 or HTTP/3, monitor connection count |
| OAuth Implementation Complexity | Requires the implementation of multiple RFCs, such as Dynamic Client Registration and Protected Resource Metadata | Delegate to verified IdPs (Okta, Keycloak) and minimize direct implementation |
| Large-scale Traffic | Operational uses of millions of requests/day are still rare in the MCP community, and discussions on next-generation delivery methods are ongoing | Track official blog updates and periodic architecture reviews are recommended |
| Dependency Security Risk | Vulnerabilities in third-party MCP packages like CVE-2025-6514 | Automated dependency auditing (pnpm audit), use only trusted packages |
RFC 9728 (Protected Resource Metadata): A standard that allows an MCP server to expose authentication server information at the /.well-known/oauth-protected-resource path, enabling clients to automatically discover where to obtain a token.
The Most Common Mistakes in Practice
- Use Long-Term Static Tokens: It is common to issue and use tokens that do not expire, similar to API keys. The core of OAuth 2.1 is short-term tokens plus automatic renewal; bypassing this can prolong the damage in the event of credential theft.
- PKCE Omission: This is the case where PKCE is not implemented based on the assumption that "it is just server-to-server communication, so it should be fine." In the MCP specification, PKCE is mandatory, not optional, and omitting it leaves you vulnerable to Authorization Code theft attacks.
- Lack of Tool-Level Access Control: This occurs when only the token is validated and it is not checked whether "this user can use this tool." This results in a situation where a user with only read permissions can call the
trigger_pipelinetool. It is strongly recommended to implement tool-specific scope mapping (TOOL_SCOPE_MAP). - Storing Plaintext Tokens: This is the case where the client-side stores the access token in plaintext in
localStorageor an environment variable file. It is safer to use encrypted storage or the OS keychain for tokens. - Failure to Implement Audit Logs: If you do not record who used which tools and when on the team shared server, it is impossible to trace the cause in the event of a security incident. It is strongly recommended that you also record denied access attempts.
In Conclusion
By combining Streamable HTTP and the OAuth 2.1 Gateway pattern, you can elevate a private, experimental MCP server into the fundamental security framework of an enterprise infrastructure that the entire team can securely share. While additional challenges remain, such as monitoring, rate limiting, and multi-tenant isolation, implementing this provides a minimum secure foundation to begin team sharing.
3 Steps to Start Right Now:
- Trading Local MCP Server to HTTP: You can update the SDK to
pnpm add @modelcontextprotocol/sdk@latestand replace the existingStdioServerTransportwithStreamableHTTPServerTransport. Connecting to a single endpoint likeapp.all("/mcp", transport.handleRequest)is the starting point. - IdP Integration Test: After launching Keycloak locally in Docker (
docker run -p 8080:8080 quay.io/keycloak/keycloak:latest start-dev), you can configure the authentication flow of the MCP server. It is important to verify that the Authorization Code Flow, including PKCE, is working correctly. - Add Gateway Middleware or Deploy Cloudflare: You can add
authMiddlewarefrom the example above to your project and define tool-specific scopes for your team inTOOL_SCOPE_MAP. If you want to reduce the burden of server operations, handling authentication at the platform layer using a Cloudflare Workers + Zero Trust combination is also a good choice. Connecting audit logs completes the basic security framework.
Next Post: In-depth Analysis of Cloudflare Durable Objects Usage Patterns for MCP Server Session Isolation and Multi-tenant Data Leak Prevention
Reference Materials
Key Resources (Recommended as a Starting Point)
- MCP Official Specification - Transports | modelcontextprotocol.io
- MCP 공식 명세 - Authorization (2025-11-25) | modelcontextprotocol.io
- MCP TypeScript SDK Official Repository | github.com
- Keycloak Official Documentation | keycloak.org
Advanced Materials
- MCP 공식 명세 - Authorization (2025-06-18) | modelcontextprotocol.io
- MCP Blog - The Future of MCP Transport Methods | blog.modelcontextprotocol.io
- Cloudflare Blog - Streamable HTTP MCP Server and Python Support | blog.cloudflare.com
- Cloudflare Blog - Zero Trust MCP Server Portals | blog.cloudflare.com
- Cloudflare Blog - Remote MCP Server Setup and Deployment | blog.cloudflare.com
- Microsoft ISE - OAuth 2.1 + Azure AD MCP Server Setup | devblogs.microsoft.com
- Spring.io - Spring AI MCP Server OAuth2 Security Applied | spring.io
- Official Introduction to Atlassian Remote MCP Server | atlassian.com
- Scalekit - Enterprise MCP Identity·SSO·Authorization | scalekit.com
- WorkOS - Creating an Enterprise Ready MCP Server | workos.com
- TrueFoundry - MCP Server Security Best Practices | truefoundry.com
- MCP OAuth 2.1, PKCE and the Future of AI Authorization | aembit.io
- fka.dev - Why MCP Abandoned SSE and Chose Streamable HTTP | blog.fka.dev
- Koyeb - Streamable HTTP Remote MCP Server Deployment | koyeb.com
- MCP Authorization Specification Update (2026) | dasroot.net