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
- What is MCP-First Architecture?
- Why MCP-First?
- Architecture Principles
- MCP-First Design Patterns
- Building MCP-First Agents
- MCP Server Development
- Integration Patterns
- Advanced Patterns
- Best Practices
- 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_secretsin 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:
- Read repository structure →
list_files(GitLab MCP tool) - Read file contents →
get_file(GitLab MCP tool) - Analyze code →
parse_typescript(Custom MCP tool?) - Generate markdown → (Agent logic, not a tool)
- 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:
- Identify use case
- List required capabilities
- Check existing tools (GitLab MCP, custom MCP)
- Design new tools for gaps
- Build MCP servers
- 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_compliancein 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_typescripttool reusable by other agents- Easy to support more languages (add
parse_pythontool) - Testing: Mock MCP tools, test agent orchestration logic
Summary: Building MCP-First at BlueFly.io
Immediate Actions
-
Audit Existing Agents
- What logic is embedded in agents?
- Can it be extracted to MCP tools?
-
Build Core MCP Servers
- compliance-mcp-server ✅ (Already designed)
- ossa-registry-mcp-server
- api-schema-mcp-server
- code-analysis-mcp-server
-
Establish MCP Server Standards
- Naming conventions
- Authentication patterns
- Error handling guidelines
- Versioning strategy
-
Create MCP Tool Registry
- Catalog of all available tools
- Documentation for each tool
- Usage examples
-
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
- Read this guide ✅
- Review existing implementations (compliance-mcp-server.py)
- Design your next agent MCP-First
- Build missing MCP servers
- Refactor existing agents to use MCP tools
- Measure impact (reusability, maintainability)
The MCP-First future is modular, composable, and infinitely extensible.