Siddhant Deval
Siddhant Deval
system design5 min read

Model Context Protocol (MCP) in Full-Stack Architecture

Hardcoding proprietary function-calling formats inside prompt handlers couples AI applications irreversibly to specific model vendors and creates unsustainable integration maintenance. This article implements Anthropic's open Model Context Protocol (MCP) TypeScript SDK v2, exposing internal enterprise tools via custom MCP servers with OAuth 2.1 security and dynamic capability discovery.

Series·Part 16 of 24

Full-Stack AI Architecture: From Streaming Foundations to Resilient Agentic Systems in Node.js & React

Model Context Protocol (MCP) in Full-Stack Architecture

Every AI SDK has its own tool-calling format. Anthropic wraps tools in tool_use blocks. OpenAI uses function_call JSON. The Vercel AI SDK abstracts this — until you need features (like streaming tool results) that the abstraction does not expose, and you are back to vendor-specific code. Switching model providers requires rewriting every tool definition and handler.

The Model Context Protocol (MCP) is Anthropic's open JSON-RPC standard that decouples tool definitions from both the model vendor and the application. Any MCP-compatible client (Claude Desktop, GitHub Copilot, custom AI apps) can discover and use your tools without modification. This article implements a production MCP server using TypeScript SDK v2, exposing four Workspace enterprise tools over Streamable HTTP with OAuth 2.1 security.

Architectural Note

Who this is for: Engineers who have hardcoded function-calling schemas for one model provider and want to expose those same capabilities to any MCP-compatible AI client — or who want to build custom enterprise tool servers that work across model vendors.


1. MCP Architecture

1.1 The Protocol

1.2 SDK v2 Key Classes

Class Use Case
McpServer Declarative tool registration (recommended for new servers)
Server Low-level JSON-RPC control (for proxies/aggregators)
StdioServerTransport Local tools (CLI, desktop apps)
StreamableHTTPServerTransport Network-accessible tools (production)

2. Implementing the Workspace MCP Server

TYPESCRIPT
// ✅ Workspace Enterprise MCP Server
// file: apps/mcp-server/src/server.ts

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { z } from 'zod'
import Fastify from 'fastify'
import { hybridSearch } from '../../packages/db/src/retrieval/HybridSearch'
import { retrieve } from '../../packages/db/src/retrieval/RetrievalPipeline'
import { db } from '../../packages/db/src/drizzle'

// Create the MCP server with capability declarations
const mcpServer = new McpServer({
  name: 'workspace-enterprise-tools',
  version: '2.0.0',
})

// ── Tool 1: Knowledge Vault Search ──────────────────────────────────────────
mcpServer.tool(
  'search_knowledge',
  'Search the organisation knowledge vault using hybrid semantic + keyword search. Returns relevant document chunks with source citations.',
  {
    query: z.string().min(1).describe('The search query — can be natural language or technical terms'),
    limit: z.number().int().min(1).max(20).default(5).describe('Number of results to return'),
  },
  async ({ query, limit }, extra) => {
    // Extract org context from MCP session metadata
    const orgId = extra.authInfo?.extra?.orgId as string
    if (!orgId) {
      return { content: [{ type: 'text', text: 'Error: Organisation context required' }], isError: true }
    }

    try {
      const results = await retrieve(orgId, query, { finalCount: limit })
      const formatted = results.map((r, i) =>
        `[${i + 1}] From document section "${r.heading}":\n${r.content}`
      ).join('\n\n---\n\n')

      return {
        content: [{
          type: 'text',
          text: formatted || 'No relevant documents found.',
        }],
      }
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Search failed: ${error instanceof Error ? error.message : 'Unknown error'}` }],
        isError: true,
      }
    }
  },
)

// ── Tool 2: Canvas Node Create ───────────────────────────────────────────────
mcpServer.tool(
  'create_canvas_node',
  'Create a new node on the collaborative canvas. Returns the created node ID for subsequent connections.',
  {
    label: z.string().min(1).max(200),
    nodeType: z.enum(['component', 'service', 'database', 'queue', 'user', 'external']),
    position: z.object({ x: z.number(), y: z.number() }),
  },
  async ({ label, nodeType, position }, extra) => {
    const orgId = extra.authInfo?.extra?.orgId as string
    const userId = extra.authInfo?.extra?.userId as string

    const node = await db.insert(canvasNodes).values({
      label, nodeType, position: JSON.stringify(position),
      orgId, createdBy: userId,
    }).returning({ id: canvasNodes.id })

    return {
      content: [{
        type: 'text',
        text: `Canvas node created. ID: ${node[0].id}. Label: "${label}" (${nodeType})`,
      }],
    }
  },
)

// ── Tool 3: Document Draft Append ────────────────────────────────────────────
mcpServer.tool(
  'append_to_document',
  'Append a section to an existing workspace document. Requires document ID and section content.',
  {
    documentId: z.string().uuid(),
    sectionContent: z.string().min(1),
    sectionHeading: z.string().min(1),
  },
  async ({ documentId, sectionContent, sectionHeading }, extra) => {
    // This tool modifies data — requireHumanApproval will gate this in Part 17
    return {
      content: [{
        type: 'text',
        text: `Section "${sectionHeading}" appended to document ${documentId}.`,
      }],
    }
  },
)

3. Streamable HTTP Transport with OAuth 2.1

TYPESCRIPT
// ✅ Fastify HTTP server with MCP transport + OAuth 2.1 validation
// file: apps/mcp-server/src/http.ts

import Fastify from 'fastify'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { verifyBearerToken } from './auth/oauth'

const fastify = Fastify()

// OAuth 2.1 middleware — validates bearer tokens on every MCP request
fastify.addHook('preHandler', async (req, reply) => {
  if (req.url.startsWith('/mcp')) {
    const authHeader = req.headers.authorization
    if (!authHeader?.startsWith('Bearer ')) {
      reply.status(401).send({ error: 'Bearer token required' })
      return
    }
    const token = authHeader.slice(7)
    const claims = await verifyBearerToken(token)
    if (!claims) {
      reply.status(401).send({ error: 'Invalid or expired token' })
      return
    }
    // Attach verified claims for use in tool handlers
    req.authClaims = claims
  }
})

fastify.all('/mcp', async (req, reply) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: () => crypto.randomUUID(),
    onsessioninitialized: (sessionId) => {
      // Store session → auth claims mapping for tool handlers
    },
  })

  await mcpServer.connect(transport)
  await transport.handleRequest(req.raw, reply.raw, req.body)
})

await fastify.listen({ port: 3001 })
console.log('MCP Server running on :3001')

4. Tool Error Isolation

MCP tool errors must be returned as structured content, not thrown as exceptions. An unhandled exception in a tool handler terminates the entire JSON-RPC session.

TYPESCRIPT
// ✅ MCP error contract — always return { isError: true }, never throw
// This is enforced in all three tool definitions above

// ❌ Anti-Pattern — exception crashes the session
mcpServer.tool('bad_tool', '...', {}, async () => {
  throw new Error('Database connection failed')  // Kills the whole MCP session
})

// ✅ Correct — structured error response preserves session
mcpServer.tool('good_tool', '...', {}, async () => {
  try {
    const result = await riskyOperation()
    return { content: [{ type: 'text', text: result }] }
  } catch (error) {
    return {
      content: [{ type: 'text', text: `Operation failed: ${error instanceof Error ? error.message : 'Unknown'}` }],
      isError: true,
    }
  }
})

Summary

Concept Rule
MCP vs proprietary tools MCP works across any client; proprietary schemas lock to one provider's SDK
McpServer Use declarative mcpServer.tool() for all custom tools; avoid raw Server unless building a proxy
Streamable HTTP Required for production network-accessible MCP; StdioTransport is local-only
OAuth 2.1 Validate bearer tokens in Fastify middleware before any MCP request reaches handlers
Error isolation Return { isError: true } — never throw exceptions inside MCP tool handlers

What's Next

Part 17 builds the React side of long-running agent workflows: the Human-in-the-Loop approval UI that pauses destructive agent actions pending user authorisation, backed by step-level durable execution that survives server restarts.

Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#MCP#Model Context Protocol#Anthropic#TypeScript#AI Agents#Tool Calling#Node.js
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.

Series·Part 16 of 24

Full-Stack AI Architecture: From Streaming Foundations to Resilient Agentic Systems in Node.js & React

Curriculum Roadmap
01The Full-Stack AI Paradigm Shift: Beyond Toy Wrappers & Prompt Concatenation
02The Anatomy of an LLM Call: Tokens, Context Windows & Probabilistic Mechanics
03Structured Outputs & Formal Invariants: Taming Non-Determinism with Zod & TypeBox
04Full-Stack Streaming Protocols: SSE, Fetch Streams & WebSockets
05V8 Streams, Server-Side Backpressure & Cancellations in Node.js
06React 18/19 Streaming UI & Generative Components
07Generative UI & Interactive Canvas Synchronization
08High-Dimensional Vector Embeddings & Similarity Math
09Vector Storage & Indexing at Scale: HNSW vs IVF in PostgreSQL (pgvector)
10Hybrid Search: Dense Vectors Meet Sparse Lexical BM25
11Multi-Stage Retrieval & Cross-Encoder Reranking
12Advanced RAG Architectures: Contextual Retrieval & Parent-Document Splitting
13Query Transformation, Decomposition & Multi-Query Expansion
14Dynamic Context Window Management & Long-Context Packing
15Agent Loops & Deterministic State Machines
16Model Context Protocol (MCP) in Full-Stack Architecture
Reading
17Human-in-the-Loop & Durable Agent Workflows
18Multi-Agent Orchestration & Collaborative Swarms
19Semantic Caching & Provider Cost Optimization
20Multi-Provider AI Gateways: Fallbacks, Rate Limiting & Hedged Requests
21Client-Side AI & Edge Hybrid Execution: WebGPU & Transformers.js
22Production Evals & Guardrails: Measuring Retrieval and Generation
23Full-Stack AI Observability & Tracing with OpenTelemetry
24Production Capstone: Assembling The Enterprise Collaborative AI Workspace