Skip to main content

MCP-First Architecture Guide

MCP-First Architecture Guide

Building Extensible, Tool-Driven AI Agents

Created: 2026-02-06 Status: Implementation Guide Context: GitLab Duo Agent Platform + BlueFly.io Platform


Table of Contents

  1. What is MCP-First Architecture?
  2. Why MCP-First?
  3. Architecture Principles
  4. MCP-First Design Patterns
  5. Building MCP-First Agents
  6. MCP Server Development
  7. Integration Patterns
  8. Advanced Patterns
  9. Best Practices
  10. Real-World Examples

What is MCP-First Architecture?

MCP-First Architecture means designing your AI agent platform where Model Context Protocol (MCP) tools are the primary interface for agent capabilities, rather than embedding logic directly in agents.

Traditional Agent Architecture

┌─────────────────────────────────────┐
│            Agent                    │
│                                     │
│  ┌──────────────────────────────┐  │
│  │   Hardcoded Business Logic   │  │
│  │   - File operations          │  │
│  │   - API calls                │  │
│  │   - Database queries         │  │
│  │   - Validation logic         │  │
│  └──────────────────────────────┘  │
│                                     │
│  System Prompt + Embedded Code     │
└─────────────────────────────────────┘

Problems:

  • Logic tightly coupled to agent
  • Hard to reuse across agents
  • Difficult to test independently
  • Versioning nightmare
  • No separation of concerns

MCP-First Architecture

┌─────────────────────────────────────────────────────────────┐
│                        Agent Layer                           │
│  (Orchestration, Decision-Making, Natural Language)         │
│                                                              │
│  Agent 1    Agent 2    Agent 3    Agent 4    Agent 5       │
│    │          │          │          │          │            │
└────┼──────────┼──────────┼──────────┼──────────┼────────────┘
     │          │          │          │          │
     └──────────┴──────────┴──────────┴──────────┘
                          │
     ┌────────────────────┴────────────────────┐
     │         MCP Protocol Layer               │
     │  (Standardized Tool Interface)           │
     └────────────────────┬────────────────────┘
                          │
     ┌────────────────────┴────────────────────┐
     │                                          │
┌────▼────┐  ┌────▼────┐  ┌────▼────┐  ┌────▼────┐
│  MCP    │  │  MCP    │  │  MCP    │  │  MCP    │
│ Server  │  │ Server  │  │ Server  │  │ Server  │
│   #1    │  │   #2    │  │   #3    │  │   #4    │
└─────────┘  └─────────┘  └─────────┘  └─────────┘
│ GitLab  │  │  OSSA   │  │Compliance│  │  API   │
│  API    │  │Registry │  │ Checker  │  │ Schema │
│  Tools  │  │  Tools  │  │  Tools   │  │ Tools  │
└─────────┘  └─────────┘  └─────────┘  └─────────┘

Benefits:

  • Separation of Concerns: Agents orchestrate, tools execute
  • Reusability: Any agent can use any tool
  • Independent Evolution: Update tools without touching agents
  • Testability: Test tools independently of agents
  • Composability: Build complex workflows from simple tools
  • Standardization: MCP protocol ensures compatibility

Why MCP-First?

1. [object Object]

Problem: You build a compliance checker in Agent A. Now Agent B also needs it.

Traditional Solution: Copy-paste code or create shared library (tight coupling)

MCP-First Solution:

# Agent A tools: - check_soc2_compliance # From MCP server # Agent B tools: - check_soc2_compliance # Same MCP server, zero duplication

Result: One implementation, infinite reuse


2. [object Object]

Scenario: You discover a bug in compliance checking logic.

Traditional: Update every agent using that logic, redeploy all agents

MCP-First: Fix the MCP server, all agents instantly benefit

MCP Server v1.0 → v1.1 (bug fix)
  │
  ├─ Agent A (automatically uses v1.1)
  ├─ Agent B (automatically uses v1.1)
  └─ Agent C (automatically uses v1.1)

3. [object Object]

MCP servers can be written in ANY language:

  • GitLab API tools: Ruby
  • OSSA Registry: Python
  • Compliance Checker: Python
  • API Schema tools: TypeScript
  • Custom logic: Go, Rust, Java, whatever fits

Agents don't care - they just call MCP tools via HTTP/WebSocket


4. [object Object]

MCP-First enables fine-grained control:

{ "agent": "security-guardian", "allowedTools": [ "check_soc2_compliance", "check_gdpr_compliance" ], "deniedTools": [ "delete_database", "modify_production_config" ] }

Traditional: Agent has full code access, hard to restrict

MCP-First: Tool-level permissions, easy audit trail


5. [object Object]

MCP servers are independently testable:

# Test compliance-mcp-server independently def test_soc2_checker(): code = "router.get('/api/users', handler)" # Missing auth findings = SOC2Checker.check(code, "test.ts") assert len(findings) == 1 assert findings[0].severity == "HIGH"

No agent needed - pure unit test

Traditional: Must test entire agent execution to test logic


Architecture Principles

Principle #1: Tools Are First-Class Citizens

Every capability should be a tool

# BAD: Embedding logic in agent system_prompt: | When checking compliance, use this regex: /password\s*=\s*['"]/ Then calculate severity based on... # GOOD: Tool does the work tools: - check_compliance

Principle #2: Agents Orchestrate, Tools Execute

Agent Role: Decision-making, workflow orchestration, natural language understanding

Tool Role: Execution, data transformation, external integration

Agent: "I need to check this code for SOC 2 compliance"
  ↓
Tool: [Executes compliance logic, returns findings]
  ↓
Agent: "I found 3 violations. Let me post them to the MR."
  ↓
Tool: [Posts comment to GitLab MR]

Principle #3: MCP Protocol as the Contract

All communication follows MCP protocol:

{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "check_soc2_compliance", "arguments": { "code": "...", "file_path": "src/api.ts" } } }

No custom protocols - everything is MCP


Principle #4: Stateless Tools, Stateful Agents

Tools: Stateless, pure functions (input → output)

Agents: Maintain conversation state, workflow state

# GOOD: Stateless tool def check_compliance(code: str) -> List[Finding]: return analyze(code) # No state stored # BAD: Stateful tool def check_compliance(code: str) -> List[Finding]: self.previous_findings.append(...) # Don't do this

Principle #5: Composability Over Monoliths

Build small, focused MCP servers:

✅ GOOD:
- compliance-mcp-server (SOC2, GDPR checks)
- ossa-registry-mcp-server (OSSA operations)
- api-schema-mcp-server (OpenAPI operations)

❌ BAD:
- everything-mcp-server (50+ tools, impossible to maintain)

MCP-First Design Patterns

Pattern #1: Capability Decomposition

Break complex capabilities into MCP tools

Example: Security Review

Monolithic Approach:

tools: - do_full_security_review # One giant tool

MCP-First Approach:

tools: # Vulnerability scanning - scan_hardcoded_secrets - scan_sql_injection - scan_xss_vulnerabilities # Compliance checking - check_soc2_compliance - check_gdpr_compliance # Dependency analysis - check_cve_database - check_license_compliance

Why Better:

  • Each tool is testable independently
  • Can reuse scan_hardcoded_secrets in other agents
  • Can version each tool separately
  • Easier to debug (know exactly which tool failed)

Pattern #2: Tool Chaining

Agents chain tools to accomplish complex tasks

Agent Flow:
1. get_merge_request_diffs → returns code changes
2. scan_hardcoded_secrets → analyzes code
3. check_soc2_compliance → validates compliance
4. generate_security_report → consolidates findings
5. post_mr_comment → publishes results

Each tool does ONE thing well


Pattern #3: Tool Parameterization

Tools accept parameters for flexibility

@mcp.tool("check_compliance") def check_compliance( code: str, frameworks: List[str] = ["SOC2", "GDPR"], # Configurable severity_threshold: str = "MEDIUM" # Configurable ) -> ComplianceResult: # Implementation

Agents can customize behavior:

# Agent A: Check SOC2 only, MEDIUM+ - tool: check_compliance params: frameworks: ["SOC2"] severity_threshold: "MEDIUM" # Agent B: Check all, CRITICAL only - tool: check_compliance params: frameworks: ["SOC2", "GDPR", "PCI"] severity_threshold: "CRITICAL"

Pattern #4: Tool Discovery

Agents discover available tools at runtime

# MCP Server exposes tool catalog @app.get("/tools") def list_tools(): return { "tools": [ { "name": "check_soc2_compliance", "description": "...", "parameters": {...} } ] }

Agents can query: "What tools are available?" → Adapt behavior


Pattern #5: Tool Versioning

MCP servers support multiple tool versions

@mcp.tool("check_compliance", version="1.0") def check_compliance_v1(...): # Original implementation @mcp.tool("check_compliance", version="2.0") def check_compliance_v2(...): # Enhanced implementation with breaking changes

Agents specify version:

tools: - name: check_compliance version: "2.0" # Explicit version pinning

Building MCP-First Agents

Step 1: Identify Capabilities as Tools

Before writing any agent, ask: "What tools do I need?"

Example: Building Documentation Generator Agent

Capabilities Needed:

  1. Read repository structure → list_files (GitLab MCP tool)
  2. Read file contents → get_file (GitLab MCP tool)
  3. Analyze code → parse_typescript (Custom MCP tool?)
  4. Generate markdown → (Agent logic, not a tool)
  5. Create file → create_file (GitLab MCP tool)

Tool Inventory:

  • list_files - Exists in GitLab MCP
  • get_file - Exists in GitLab MCP
  • ⚠️ parse_typescript - Need custom MCP server
  • generate_markdown - Agent handles this (LLM capability)
  • create_file - Exists in GitLab MCP

Decision: Build code-analysis-mcp-server with parse_typescript tool


Step 2: Design Agent as Orchestrator

Agent configuration:

name: docs-generator description: Generate documentation from code system_prompt: | You are a documentation generator. Your workflow: 1. Use list_files to find source files 2. Use get_file to read each file 3. Use parse_typescript to extract types/functions 4. Generate markdown documentation (your core skill) 5. Use create_file to save documentation You ORCHESTRATE tools. You DON'T contain business logic. tools: - list_files - get_file - parse_typescript # Custom MCP - create_file

Key: Agent describes HOW to use tools, not WHAT tools do


Step 3: Build Missing MCP Servers

For each custom tool, create MCP server:

# code-analysis-mcp-server.py @mcp.tool("parse_typescript") def parse_typescript(code: str, file_path: str) -> ParsedCode: """Parse TypeScript code and extract types, functions, etc.""" # Use typescript parser library ast = parse_ts(code) return { "types": extract_types(ast), "functions": extract_functions(ast), "exports": extract_exports(ast) }

Step 4: Connect Agent to MCP Servers

Configure MCP client:

// .gitlab/duo/mcp.json { "mcpServers": { "code-analysis": { "type": "http", "url": "https://code-analysis-mcp.internal/mcp", "approvedTools": ["parse_typescript"] } } }

Step 5: Test Tools Independently

Before testing agent, test tools:

# Test custom MCP tool curl -X POST https://code-analysis-mcp.internal/mcp/tools/parse_typescript \ -H "Content-Type: application/json" \ -d '{ "code": "export function hello() { return \"world\"; }", "file_path": "test.ts" }'

If tools work, agent will work


MCP Server Development

MCP Server Anatomy

from fastapi import FastAPI from mcp import MCPServer # Conceptual app = FastAPI() mcp = MCPServer() # Tool 1: Business logic @mcp.tool("tool_name") def my_tool(param1: str, param2: int) -> dict: # Implementation return result # Tool 2: Another capability @mcp.tool("another_tool") def another_tool(data: str) -> str: return processed_data # Expose tools via HTTP @app.post("/tools/{tool_name}") def invoke_tool(tool_name: str, request: ToolRequest): return mcp.invoke(tool_name, request.params) # Tool discovery @app.get("/tools") def list_tools(): return mcp.list_tools()

MCP Server Best Practices

1. [object Object]

✅ GOOD:
- compliance-mcp-server: All compliance checking
- gitlab-api-mcp-server: All GitLab operations
- ossa-registry-mcp-server: All OSSA operations

❌ BAD:
- mixed-mcp-server: Compliance + GitLab + Random stuff

2. [object Object]

# GOOD: Stateless def check_compliance(code: str) -> List[Finding]: findings = analyze_code(code) # Pure function return findings # BAD: Stateful class ComplianceChecker: def __init__(self): self.findings = [] # State def check_compliance(self, code: str): self.findings.append(...) # Mutates state

Why: Stateless tools are:

  • Thread-safe
  • Cacheable
  • Testable
  • Predictable

3. [object Object]

@mcp.tool("risky_operation") def risky_operation(data: str) -> Result: try: result = dangerous_operation(data) return {"success": True, "data": result} except ValidationError as e: return {"success": False, "error": "Invalid input", "details": str(e)} except NetworkError as e: return {"success": False, "error": "Network failure", "details": str(e)}

Agents need to know WHY tools fail


4. [object Object]

from pydantic import BaseModel, Field class ComplianceRequest(BaseModel): code: str = Field(..., min_length=1, description="Code to analyze") file_path: str = Field(..., pattern=r"^[a-zA-Z0-9/_.-]+$") frameworks: List[str] = Field(default=["SOC2", "GDPR"]) @mcp.tool("check_compliance") def check_compliance(request: ComplianceRequest) -> ComplianceResult: # request is validated automatically ...

5. [object Object]

@mcp.tool("check_compliance") def check_compliance(code: str, frameworks: List[str]) -> dict: """ Check code for compliance violations Args: code: Source code to analyze (any language) frameworks: List of frameworks to check (SOC2, GDPR, PCI, etc.) Returns: { "compliant": bool, "findings": [ { "framework": "SOC2", "severity": "HIGH", "message": "...", "remediation": "..." } ] } Examples: >>> check_compliance("const password = '123'", ["SOC2"]) {"compliant": False, "findings": [...]} """ ...

This documentation is exposed via /tools endpoint


Integration Patterns

Pattern #1: GitLab MCP + Custom MCP (Hybrid)

Most common pattern for BlueFly.io:

# Agent configuration tools: # GitLab native MCP tools - get_merge_request - get_merge_request_diffs - create_workitem_note # Custom MCP tools - check_soc2_compliance # compliance-mcp-server - check_ossa_manifest # ossa-registry-mcp-server - validate_openapi_spec # api-schema-mcp-server

MCP Client Config:

{ "mcpServers": { "gitlab": { "type": "builtin", // Native GitLab MCP "approvedTools": ["get_merge_request", "..."] }, "compliance": { "type": "http", "url": "https://compliance-mcp.internal/mcp", "approvedTools": ["check_soc2_compliance", "..."] } } }

Pattern #2: MCP Tool Gateway

Single gateway for all custom MCP servers:

Agent → MCP Gateway → Route to specific MCP server
                  ├─ compliance-mcp-server
                  ├─ ossa-registry-mcp-server
                  └─ api-schema-mcp-server

Benefits:

  • Single authentication point
  • Centralized logging/monitoring
  • Rate limiting
  • Caching layer

Implementation (Nginx proxy):

location /mcp/compliance/ { proxy_pass https://compliance-mcp.internal/; } location /mcp/ossa-registry/ { proxy_pass https://ossa-registry-mcp.internal/; }

Pattern #3: MCP Tool Composition

Tools that call other tools:

@mcp.tool("comprehensive_security_scan") def comprehensive_scan(code: str) -> dict: """Meta-tool that orchestrates multiple security tools""" # Call other MCP tools vuln_findings = call_mcp_tool("scan_vulnerabilities", {"code": code}) compliance_findings = call_mcp_tool("check_compliance", {"code": code}) dependency_findings = call_mcp_tool("scan_dependencies", {"code": code}) # Consolidate return { "vulnerabilities": vuln_findings, "compliance": compliance_findings, "dependencies": dependency_findings }

Use sparingly - prefer agent orchestration


Advanced Patterns

Pattern #1: Streaming MCP Tools

For long-running operations:

@mcp.tool("analyze_large_codebase", streaming=True) async def analyze_codebase(repo_url: str): """Stream results as analysis progresses""" yield {"status": "cloning", "progress": 10} repo = clone_repo(repo_url) yield {"status": "analyzing", "progress": 50} for file in repo.files: result = analyze_file(file) yield {"file": file, "result": result} yield {"status": "complete", "progress": 100}

Agent receives updates in real-time


Pattern #2: MCP Tool Authentication

Tools that need credentials:

@mcp.tool("call_external_api") def call_external_api(endpoint: str, auth_token: str) -> dict: """Call external API with authentication""" # Validate token format if not is_valid_token(auth_token): raise ValueError("Invalid auth token") # Call API response = requests.post( endpoint, headers={"Authorization": f"Bearer {auth_token}"} ) return response.json()

Agent passes credentials (from secure storage)


Pattern #3: MCP Tool Caching

Cache expensive operations:

from functools import lru_cache @lru_cache(maxsize=1000) @mcp.tool("expensive_analysis") def expensive_analysis(code_hash: str) -> dict: """Expensive analysis with caching""" # Only runs if not in cache result = perform_expensive_analysis(code_hash) return result

Same input → Instant cached result


Best Practices

1. [object Object]

Process:

  1. Identify use case
  2. List required capabilities
  3. Check existing tools (GitLab MCP, custom MCP)
  4. Design new tools for gaps
  5. Build MCP servers
  6. THEN build agent

Don't: Build agent, realize you need tools, hack them in


2. [object Object]

✅ GOOD:
- check_soc2_compliance
- validate_openapi_spec
- scan_vulnerabilities
- list_ossa_agents

❌ BAD:
- doCompliance
- validate
- scan
- getAgents

Pattern: <verb>_<noun>_<optional_specifier>


3. [object Object]

# Explicit versioning @mcp.tool("check_compliance", version="1.0") def check_compliance_v1(...): # Original implementation @mcp.tool("check_compliance", version="2.0") def check_compliance_v2(...): # Breaking changes # Agents specify version tools: - name: check_compliance version: "2.0" # Explicit - name: check_compliance # Defaults to latest

4. [object Object]

# Unit test: Tool logic only def test_compliance_checker(): code = "const password = '123'" findings = check_compliance(code, ["SOC2"]) assert len(findings) > 0 # Integration test: MCP server def test_mcp_server(): response = requests.post("/tools/check_compliance", json={...}) assert response.status_code == 200 # End-to-end test: Agent + MCP server def test_agent_with_mcp(): agent = create_agent(tools=["check_compliance"]) result = agent.execute("Check this code") assert result.success

5. [object Object]

# Logging logger.info(f"Tool invoked: {tool_name}", extra={ "agent": agent_id, "parameters": sanitize(params), "duration_ms": duration }) # Metrics prometheus.counter("mcp_tool_invocations_total", { "tool": tool_name, "status": "success" }).inc() # Tracing with tracer.span("mcp_tool_execution") as span: span.set_attribute("tool.name", tool_name) result = execute_tool(tool_name, params)

Real-World Examples

Example #1: Security Guardian Agent (MCP-First)

Architecture:

Security Guardian Agent
  │
  ├─ get_merge_request_diffs (GitLab MCP)
  ├─ scan_hardcoded_secrets (Custom MCP)
  ├─ scan_sql_injection (Custom MCP)
  ├─ check_soc2_compliance (Custom MCP)
  ├─ check_gdpr_compliance (Custom MCP)
  ├─ scan_dependencies (Custom MCP)
  └─ create_workitem_note (GitLab MCP)

Why MCP-First?

  • Each security check is independently testable
  • Can reuse check_soc2_compliance in other agents
  • Easy to add new checks (just add tool to MCP server)
  • Non-security agents can use get_merge_request_diffs

Example #2: OSSA Compliance Validator (MCP-First)

Tools Used:

# GitLab MCP tools - get_file # Read OSSA manifest - semantic_code_search # Find related configs - create_workitem_note # Post findings # Custom MCP tools - validate_ossa_schema # ossa-registry-mcp-server - check_deprecated_patterns # ossa-registry-mcp-server - check_security_config # compliance-mcp-server

Agent Role: Orchestrate validation flow, format output

Tool Role: Execute validation logic


Example #3: Documentation Generator (MCP-First)

Workflow:

1. list_files (GitLab MCP) → Get all TypeScript files
2. get_file (GitLab MCP) → Read each file
3. parse_typescript (Custom MCP) → Extract types/functions
4. [Agent generates markdown from parsed data]
5. create_file (GitLab MCP) → Save documentation
6. create_commit (GitLab MCP) → Commit changes

MCP-First Benefits:

  • parse_typescript tool reusable by other agents
  • Easy to support more languages (add parse_python tool)
  • Testing: Mock MCP tools, test agent orchestration logic

Summary: Building MCP-First at BlueFly.io

Immediate Actions

  1. Audit Existing Agents

    • What logic is embedded in agents?
    • Can it be extracted to MCP tools?
  2. Build Core MCP Servers

    • compliance-mcp-server ✅ (Already designed)
    • ossa-registry-mcp-server
    • api-schema-mcp-server
    • code-analysis-mcp-server
  3. Establish MCP Server Standards

    • Naming conventions
    • Authentication patterns
    • Error handling guidelines
    • Versioning strategy
  4. Create MCP Tool Registry

    • Catalog of all available tools
    • Documentation for each tool
    • Usage examples
  5. Train Team on MCP-First Thinking

    • "Tool first, agent second"
    • Design tools as reusable capabilities
    • Agents are orchestrators, not executors

Long-Term Vision

Goal: Every capability at BlueFly.io is an MCP tool

BlueFly.io MCP Tool Ecosystem:

├─ GitLab Operations
│  ├─ Issues, MRs, Pipelines (GitLab native MCP)
│
├─ Compliance & Security
│  ├─ SOC2, GDPR, PCI checks
│  ├─ Vulnerability scanning
│  └─ License compliance
│
├─ OSSA Platform
│  ├─ Agent registry operations
│  ├─ Manifest validation
│  └─ Deployment automation
│
├─ API Management
│  ├─ OpenAPI validation
│  ├─ Type generation
│  └─ Breaking change detection
│
├─ Code Quality
│  ├─ Linting
│  ├─ Code parsing
│  └─ Complexity analysis
│
└─ Infrastructure
   ├─ Kubernetes operations
   ├─ Database migrations
   └─ Service health checks

Result: Any agent can leverage ANY capability instantly


Next Steps

  1. Read this guide
  2. Review existing implementations (compliance-mcp-server.py)
  3. Design your next agent MCP-First
  4. Build missing MCP servers
  5. Refactor existing agents to use MCP tools
  6. Measure impact (reusability, maintainability)

The MCP-First future is modular, composable, and infinitely extensible.