logicspike/docs

MCP (Model Context Protocol)

Vlozi MCP Architecture Plan

Last Updated: 2026-05-14 Status: Planning — Phase 1 not yet started Owner: Platform Team

This document is the single source of truth for how Vlozi designs, builds, and ships MCP (Model Context Protocol) servers across all products. Read this before writing a single line of MCP code.


1. What is MCP and Why We're Building It

MCP is a standard protocol that lets an AI agent (Claude, Cursor, a customer's autonomous agent) call tools you expose — structured functions with typed inputs and outputs. It's like a REST API, except the caller is an AI, not a browser.

Without MCP, an AI agent working with Vlozi has to navigate our REST APIs raw — it must know URL paths, auth headers, and response shapes. With MCP, the agent calls blog.list_drafts and gets back structured data. The AI doesn't need to know anything about our internals.

Why this matters for Vlozi specifically:

  • The AI Brain and Contact Intelligence are only as powerful as the tools available to them. MCP is the connective tissue.
  • Customers will want to connect their own AI agents (Claude, GPT, custom) to their Vlozi workspace. MCP is the standard interface.
  • Our own dev tooling (Claude Code, Cursor) gets dramatically better when it can call Vlozi tools directly during development.

2. Architecture Decision: Dedicated MCP Gateway

We evaluated three patterns. We are implementing Option D: Dedicated apps/mcp-gateway Worker.

Why Not Option A (Single Central MCP Server)

A single vlozi-mcp package with all tools collapses when we need per-tenant tool filtering — a blog-only customer must not see brain.* tools.

Why Not Option B (Independent Per-Product MCP Servers)

Customers would need to configure N MCP servers in their AI client. Operational overhead is N times higher. Cross-product workflows require the AI to stitch across servers.

Why Not Option C (MCP Inside apps/gateway)

Mixing MCP into the existing REST gateway bloats its bundle with the MCP SDK (~400KB), inflating cold starts for every REST API call — including dashboard requests. MCP traffic (agent-driven, bursty, long-lived SSE connections) has a completely different profile from REST traffic (human-driven, short-lived). A runaway agent loop would compete with dashboard users for the same Worker. An MCP deploy bug could take down the REST gateway. They must be separate.

Option D — What We're Building

REST traffic   →  apps/gateway     →  product workers  (unchanged, stays lean)
MCP traffic    →  apps/mcp-gateway →  product workers  (dedicated, MCP SDK lives here)
 
AI Agent (Claude / Cursor / Customer Agent)

              │  Single MCP connection  (mcp.vlozi.app)

┌──────────────────────────────────────────────┐
│            apps/mcp-gateway                   │
│                                               │
│  • Authenticates the caller (API key)         │
│  • Resolves tenant context                    │
│  • Filters tools by subscription plan         │
│  • Routes call to the right product Worker    │
│  • Logs every tool call for audit             │
│  • stdio transport for dev tooling            │
│  • HTTP/SSE transport for production          │
└──────┬───────┬────────┬───────┬──────────────┘
       │       │        │       │
  blog-svc brain-svc  CI-svc  newsletter-svc
 (internal)(internal)(internal)(internal)

apps/mcp-gateway is the only external MCP surface. apps/gateway handles REST only — it never changes for MCP. Both workers share service bindings to the same product Workers and the same KV namespace.

Auth logic is shared via packages/mcp-core — no duplication between the two gateways.


3. Monorepo Structure

packages/
  mcp-core/                   ← NEW: shared MCP types, schema builders, auth helpers
    src/
      types.ts                ← McpTool, McpResource, McpContext interfaces
      schema.ts               ← zod-to-MCP schema converter
      auth.ts                 ← API key validation shared by mcp-gateway and tests
      errors.ts               ← standard MCP error shapes
    package.json
 
apps/
  gateway/                    ← UNCHANGED — REST only, no MCP code ever goes here
 
  mcp-gateway/                ← NEW dedicated MCP Worker
    src/
      index.ts                ← Worker entry point — registers both transports
      server.ts               ← assembles all product tool registries into one MCP server
      router.ts               ← namespace routing: blog.* → BLOG_SERVICE binding
      auth.ts                 ← wraps packages/mcp-core auth + plan-based tool filtering
      transports/
        http-sse.ts           ← production transport (Cloudflare Workers SSE)
        stdio.ts              ← dev transport (local Node.js process)
      resources/
        workspace.ts          ← workspace-level MCP resources
    wrangler.toml             ← service bindings to all product Workers + shared KV
    package.json
 
  blog-service/
    src/
      mcp/                    ← blog MCP tools, co-located with the service
        tools/
          list-posts.ts
          create-draft.ts
          publish-post.ts
          get-analytics.ts
        index.ts              ← exports: { namespace: 'blog', tools: [...] }
 
  brain-service/
    src/
      mcp/
        tools/
          query.ts
          ingest.ts
          list-memories.ts
        index.ts
 
  contact-intelligence/
    src/
      mcp/
        tools/
          search-contacts.ts
          get-contact-memory.ts
          enrich-contact.ts
          log-interaction.ts
        index.ts
 
  newsletter-service/
    src/
      mcp/
        tools/
          list-campaigns.ts
          get-analytics.ts
          create-campaign.ts
        index.ts
 
  content-engine/
    src/
      mcp/                    ← already spec'd in docs/content-engine/mcp-server-spec.md
        tools/
          ...                 ← migrate existing spec into this folder
        index.ts
 
  chat-engine/
    src/
      mcp/
        tools/
          list-sessions.ts
          get-session.ts
        index.ts
 
  media-service/
    src/
      mcp/
        tools/
          upload.ts
          list-assets.ts
          search-assets.ts
        index.ts

Rule: MCP tools live co-located with the service that owns the data. No standalone mcp-server apps. apps/mcp-gateway assembles them — it imports each service's mcp/index.ts registry.

Rule: apps/gateway never imports anything from apps/mcp-gateway and vice versa. They are independent Workers that share only packages/mcp-core.

Problem: apps/mcp-gateway cannot import from service apps (Rule 7.2). Solution: Each service's mcp/index.ts exports only tool descriptors (name, description, schema, handler URL) — no business logic. mcp-gateway proxies the actual execution to the service over the Cloudflare service binding.


4. Tool Descriptor Pattern

Every service exports its MCP tools as descriptors, not live functions. mcp-gateway resolves them.

// apps/blog-service/src/mcp/index.ts
import type { McpToolDescriptor } from '@vlozi/mcp-core'
 
export const namespace = 'blog'
 
export const tools: McpToolDescriptor[] = [
  {
    name: 'list_posts',
    description: 'List blog posts for the current workspace. Supports filtering by status, tag, and date range.',
    inputSchema: {
      type: 'object',
      properties: {
        status: { type: 'string', enum: ['draft', 'published', 'scheduled'], description: 'Filter by post status' },
        limit:  { type: 'number', description: 'Max results (default 20, max 100)' },
        cursor: { type: 'string', description: 'Pagination cursor from previous response' },
      },
    },
    // mcp-gateway calls this internal endpoint when the tool is invoked
    internalRoute: 'GET /mcp/tools/list-posts',
    // PBAC permission required
    requiredPermission: 'blog:posts.read',
    // which plan gates this tool (undefined = all plans)
    requiredPlan: undefined,
  },
  {
    name: 'create_draft',
    description: 'Create a new blog post draft.',
    inputSchema: {
      type: 'object',
      required: ['title'],
      properties: {
        title:   { type: 'string' },
        content: { type: 'string', description: 'MDX content body' },
        tags:    { type: 'array', items: { type: 'string' } },
        slug:    { type: 'string', description: 'URL slug (auto-generated from title if omitted)' },
      },
    },
    internalRoute: 'POST /mcp/tools/create-draft',
    requiredPermission: 'blog:posts.write',
  },
]

The internalRoute is an endpoint the service exposes only to mcp-gateway (verified via x-gateway-key). mcp-gateway receives the MCP call, resolves the descriptor, injects tenant context, and proxies to the service over the service binding.


5. Transport Strategy

Transport When Where
stdio Dev tooling — Claude Code, Cursor Local Node.js process, .mcp.json
HTTP + SSE Customer-facing, production apps/mcp-gateway on Cloudflare Workers, mcp.vlozi.app

5.1 Dev Transport (stdio)

The stdio entrypoint is a plain Node.js script inside apps/mcp-gateway. It runs locally against your dev services — no Cloudflare deployment needed.

Add to root .mcp.json:

{
  "mcpServers": {
    "code-review-graph": {
      "command": "code-review-graph",
      "args": ["serve"],
      "type": "stdio"
    },
    "vlozi": {
      "command": "node",
      "args": ["./apps/mcp-gateway/dist/transports/stdio.js"],
      "type": "stdio",
      "env": {
        "MCP_API_KEY": "${VLOZI_DEV_API_KEY}",
        "MCP_TENANT_ID": "${VLOZI_DEV_TENANT_ID}"
      }
    }
  }
}

Just turbo dev and the MCP server is live. The stdio script connects directly to locally running product services.

5.2 Production Transport (HTTP/SSE)

Endpoint: https://mcp.vlozi.app/sse

This runs inside apps/mcp-gateway on Cloudflare Workers. apps/gateway is not involved. The SSE transport:

  1. Client opens a long-lived SSE connection with Authorization: Bearer ls_xxx
  2. mcp-gateway authenticates and resolves tenant
  3. mcp-gateway streams back the tool list filtered to the tenant's plan
  4. Client sends tool calls as POST to https://mcp.vlozi.app/message
  5. mcp-gateway proxies to the appropriate product Worker via service binding and streams the result back

Standard MCP SSE transport spec — any MCP-compatible client works out of the box.


6. Authentication & Tenant Resolution

Client sends:  Authorization: Bearer ls_xxx
                             OR
               x-api-key: ls_xxx
 
mcp-gateway (using packages/mcp-core auth):
  1. Validates API key — KV cache first (5min TTL), DB only on miss
  2. Resolves tenantId from key metadata
  3. Loads key's permission scopes
  4. Builds McpContext { tenantId, permissions, plan }
 
Per tool call:
  5. Checks tool.requiredPermission ∈ context.permissions
  6. Checks tool.requiredPlan satisfied by context.plan
  7. If either fails → MCP error: "permission_denied"
  8. Injects x-tenant-id + x-gateway-key into internal service call
  9. Logs: { tenantId, toolName, agentId, durationMs, status }

API keys for MCP are the same ls_ keys used for REST API access — no new auth system. packages/mcp-core/src/auth.ts contains the shared validation logic; apps/gateway and apps/mcp-gateway both import it, so there is no duplication.

Permission Mapping (all products)

Tool Required Permission
blog.list_posts blog:posts.read
blog.create_draft blog:posts.write
blog.publish_post blog:posts.publish
blog.get_analytics blog:analytics.read
brain.query brain:memory.read
brain.ingest brain:memory.write
brain.list_memories brain:memory.read
contacts.search contacts:read
contacts.get_memory contacts:memory.read
contacts.enrich contacts:write
contacts.log_interaction contacts:interactions.write
newsletter.list_campaigns newsletter:read
newsletter.get_analytics newsletter:analytics.read
newsletter.create_campaign newsletter:write
content.* See docs/content-engine/mcp-server-spec.md
media.list_assets media:read
media.upload media:write
chat.list_sessions chat:sessions.read

7. Full Tool Catalog

7.1 Blog (blog.*)

Tool Description
blog.list_posts List posts with status/tag/date filters
blog.get_post Get full post content and metadata by slug or ID
blog.create_draft Create a new draft
blog.update_post Update title, content, tags, slug of a draft
blog.publish_post Publish a draft immediately or schedule it
blog.unpublish_post Revert a published post to draft
blog.get_analytics Views, reads, scroll depth for a post or date range
blog.list_tags List all tags in the workspace
blog.search_posts Full-text search across posts

7.2 AI Brain (brain.*)

Tool Description
brain.query Semantic search across workspace memory
brain.ingest Add a document or fact to workspace memory
brain.list_memories List recent memories with optional category filter
brain.delete_memory Remove a specific memory by ID
brain.get_context Get assembled context for a given topic (used by agents before composing)

7.3 Contact Intelligence (contacts.*)

Tool Description
contacts.search Search contacts by name, email, company, tag
contacts.get_contact Get full contact profile by ID
contacts.get_memory Get AI memory for a specific contact
contacts.enrich Trigger enrichment for a contact
contacts.log_interaction Record an interaction (email sent, call, meeting)
contacts.get_outreach_context Get assembled context for drafting outreach
contacts.list_tags List all contact tags

7.4 Newsletter (newsletter.*)

Tool Description
newsletter.list_campaigns List campaigns with status filter
newsletter.get_campaign Get campaign details and content
newsletter.create_campaign Create a new campaign draft
newsletter.get_analytics Open rate, click rate, unsub rate for a campaign
newsletter.list_subscribers List subscribers with segment filter
newsletter.get_subscriber_count Count subscribers by segment

7.5 Content Engine (content.*)

Full tool spec is in docs/content-engine/mcp-server-spec.md. Summary:

Tool Description
content.schedule_content Schedule a social post
content.get_calendar Get content calendar for a date range
content.bulk_schedule Schedule multiple posts at once
content.list_connections List connected social accounts
content.get_publish_status Check publish result for a slot

7.6 Media (media.*)

Tool Description
media.list_assets List media assets with type/tag filter
media.get_asset Get asset URL and metadata by ID
media.search_assets Search assets by name or tag
media.upload Upload a new asset (returns upload URL)
media.delete_asset Delete an asset by ID

7.7 Chat Engine (chat.*)

Read-only tools for agents to query conversation history and session state.

Tool Description
chat.list_sessions List chatbot sessions with date/status filter
chat.get_session Get full session transcript by ID
chat.get_session_stats Message count, avg response time, handoff rate

8. MCP Resources

Resources are read-only context that agents can access without calling a tool. Think of them as live dashboards an agent checks before acting.

Resource URI Description
vlozi://workspace Workspace name, plan, active products
vlozi://blog/recent Last 5 published posts
vlozi://blog/drafts All current drafts
vlozi://contacts/recent Last 10 updated contacts
vlozi://newsletter/stats Current subscriber count + last campaign stats
vlozi://content/calendar/week This week's content calendar
vlozi://brain/recent Last 10 memory entries
vlozi://media/recent Last 10 uploaded assets

9. How to Add a New MCP Tool (Step-by-Step for Devs)

This is the repeatable process every developer follows when adding a tool.

Step 1 — Define the descriptor

In your service's src/mcp/index.ts, add an entry to the tools array:

{
  name: 'my_tool',
  description: 'One sentence — what this does and when an agent should call it.',
  inputSchema: {
    type: 'object',
    required: ['requiredField'],
    properties: {
      requiredField: { type: 'string', description: 'What this field is' },
      optionalField: { type: 'number', description: 'What this field is' },
    },
  },
  internalRoute: 'POST /mcp/tools/my-tool',
  requiredPermission: 'product:resource.action',
}

Step 2 — Implement the internal route handler

In your service, add a Hono route that:

  1. Verifies x-gateway-key header
  2. Reads x-tenant-id header (never from body)
  3. Executes the business logic
  4. Returns { data: ..., error: null } on success or { data: null, error: { code, message } } on failure
// apps/blog-service/src/routes/mcp-tools.ts
app.post('/mcp/tools/my-tool', verifyGatewayKey, async (c) => {
  const tenantId = c.req.header('x-tenant-id')!
  const body = await c.req.json()
  // ... business logic ...
  return c.json({ data: result, error: null })
})

Step 3 — Register in the mcp-gateway router

In apps/mcp-gateway/src/router.ts, the service registry auto-discovers tools via the descriptor import. If you added the descriptor in Step 1, mcp-gateway picks it up automatically at next deployment.

Step 4 — Add the permission to your permission matrix

In packages/core-access, add the new permission string to the enum and set which default roles receive it.

Step 5 — Test with the stdio dev server

# Start your service locally
turbo dev --filter=blog-service
 
# In another terminal — run the mcp-gateway stdio server
node apps/mcp-gateway/dist/transports/stdio.js
 
# Claude Code or Cursor now has the tool available

Step 6 — Write a tool test

// apps/blog-service/src/mcp/tools/__tests__/my-tool.test.ts
it('returns posts for valid tenant', async () => {
  const result = await callMcpTool('blog.my_tool', {
    requiredField: 'value',
  }, { tenantId: TEST_TENANT_ID })
  expect(result.error).toBeNull()
  expect(result.data).toBeDefined()
})

10. Rate Limits & Safety Guards

These are non-negotiable and enforced in apps/mcp-gateway/src/auth.ts.

Guard Value Reason
Max tool calls / minute / tenant 120 Prevent runaway agent loops
Max tool calls / minute / API key 60 Per-key throttle
Write tools require explicit permission Mandatory Read-only agents cannot mutate
All tool calls logged with agentId Mandatory Audit trail, billing
dryRun: true available on all write tools Optional Agents can preview before acting
Max items returned per list tool 100 Prevent memory bloat in agent context
Bulk operations capped per call 50 Prevent calendar/DB flooding

The agentId header is optional for human-initiated calls but required for agent-initiated calls. Agents must self-identify. The gateway rejects bulk write operations from keys without agentId.


11. Implementation Phases

Phase 1 — Dev Tooling Foundation (Target: Week 1)

Goal: Claude Code and Cursor can call Vlozi tools during development. Zero customer impact.

Deliverables:

  • packages/mcp-core — shared types: McpToolDescriptor, McpContext, McpError, shared auth util
  • apps/mcp-gateway scaffold — wrangler.toml, package.json, tsconfig.json
  • apps/mcp-gateway/src/server.ts — MCP server skeleton
  • apps/mcp-gateway/src/auth.ts — API key → McpContext resolution (uses mcp-core)
  • apps/mcp-gateway/src/router.ts — namespace routing skeleton
  • apps/mcp-gateway/src/transports/stdio.ts — stdio transport for local dev
  • Blog service MCP descriptors + internal route handlers (all 9 tools)
  • Brain service MCP descriptors + internal route handlers (all 5 tools)
  • Updated .mcp.json with vlozi stdio server entry
  • Dev setup guide in this file (Section 12)

Done when: A developer can open Claude Code and ask "list my blog drafts" and get a real response.


Phase 2 — Content Engine + Contact Intelligence MCP (Target: Week 2)

Goal: The AI Brain can autonomously operate the content calendar and access contact context.

Deliverables:

  • Migrate existing docs/content-engine/mcp-server-spec.md tools into content-engine/src/mcp/
  • Contact Intelligence MCP descriptors + internal route handlers (all 7 tools)
  • Newsletter MCP descriptors + internal route handlers (all 6 tools)
  • Media MCP descriptors (all 5 tools)
  • apps/mcp-gateway router updated for all new namespaces
  • Tool permission matrix complete in packages/core-access

Done when: An agent can query a contact's memory, check the content calendar, and schedule a post — in one conversation.


Phase 3 — HTTP/SSE Production Transport (Target: With Blog v1.0)

Goal: Customers can connect their AI assistant to their Vlozi workspace via mcp.vlozi.app.

Deliverables:

  • apps/mcp-gateway/src/transports/http-sse.ts — Cloudflare Workers SSE transport
  • mcp.vlozi.app subdomain configured and routed to apps/mcp-gateway (not apps/gateway)
  • CORS + auth for external connections
  • Per-plan tool filtering (blog-only plan sees only blog.* tools)
  • MCP connection UI in seller dashboard (connect/disconnect, see tool call log)
  • Public documentation at docs.vlozi.app/mcp
  • Rate limit enforcement for production traffic

Done when: A customer can paste mcp.vlozi.app into Claude Desktop and query their blog.


Phase 4 — Agent Personas & Automation (Post-launch)

Goal: First-party agent personas that customers can enable from the dashboard.

These are pre-built agents that use the MCP tools under the hood:

Persona What it does
Content Strategist Checks content calendar gaps, drafts social posts from blog content, schedules automatically
Blog Promoter Triggered on post publish — generates platform-specific copies, bulk schedules across connections
Outreach Drafter Given a contact ID, pulls memory + recent interactions, drafts a personalized outreach email
Newsletter Curator Analyzes recent blog posts and brain memories, drafts a newsletter campaign

Each persona is a Claude agent with a system prompt and a restricted set of MCP tools. Customers toggle them on/off per workspace.


12. Local Dev Setup

Prerequisites

  • Turbo dev stack running (turbo dev)
  • A dev API key with all scopes (generate in seller dashboard or seed script)
  • Dev tenant ID

Setup

# 1. Build the mcp-gateway stdio entrypoint
turbo build --filter=mcp-gateway
 
# 2. Set env vars (never commit these)
export VLOZI_DEV_API_KEY=ls_dev_xxxx
export VLOZI_DEV_TENANT_ID=tenant_xxxx
 
# 3. The .mcp.json is already configured — restart Claude Code / Cursor
# Tools will appear in the tool picker immediately

Verifying tools are loaded

In Claude Code, type: what vlozi tools do you have access to? — Claude will list all available tools from the MCP server.

Troubleshooting

Symptom Likely cause Fix
No tools appear stdio server not built Run turbo build --filter=mcp-gateway
permission_denied on all tools API key lacks scopes Regenerate key with all scopes
Tool call returns 500 Service not running locally Run turbo dev --filter=blog-service
Stale tool list MCP server still running old build Restart Claude Code

13. Key Decisions Log

Decision Rationale
Dedicated apps/mcp-gateway, not MCP inside apps/gateway MCP SDK (~400KB) must not bloat the REST gateway cold start. MCP traffic (agent-driven, SSE, bursty) and REST traffic (human, short-lived) have different profiles and must scale independently. A bad MCP deploy cannot take down the dashboard.
apps/mcp-gateway built from the start (Phase 1), not later Starting with the correct structure costs nothing extra. Avoids a painful migration from gateway-embedded MCP to a dedicated Worker when Phase 3 arrives.
Auth shared via packages/mcp-core, not duplicated apps/gateway and apps/mcp-gateway use the same API key validation logic. One change propagates to both.
Tools co-located with services, not in a standalone package Prevents descriptor drift from implementation. Tool and handler change together.
Descriptor-based routing (not live function import) Respects Rule 7.2 (No Cross-App Imports). mcp-gateway proxies over service bindings; it does not import service logic.
Same API keys for REST and MCP One key management UI. Existing PBAC permission system reused unchanged.
stdio for dev, SSE for production stdio needs zero infra. SSE is the MCP standard for deployed servers.
agentId required for bulk writes Agents must be identifiable for audit and billing. Anonymous bulk mutations are too risky.
MCP (Model Context Protocol)