Skip to main content

Overview

The Model Context Protocol (MCP) is a standardized protocol for connecting AI agents to external tools and data sources. Pricing Intelligence implements MCP to enable Harvey (the AI agent) to invoke pricing analysis tools without tight coupling.
Pricing Intelligence follows the MCP 2025-06-18 specification for both server and client implementations.

Why MCP?

Traditional AI agent architectures face several challenges:

Tight Coupling

Agent code directly calls backend APIs, making changes brittle

Secret Sharing

Agents need API keys for every service they call

No Standardization

Every integration uses different protocols and formats

Poor Observability

Difficult to trace what tools are being invoked and why
MCP solves these by:
  • Standardizing tool invocation with JSON-RPC
  • Abstracting backend services behind a protocol layer
  • Centralizing authentication at the server level
  • Enabling introspection (agents can discover available tools)

MCP Architecture

Pricing Intelligence uses MCP to connect Harvey (client) to the MCP Server (server):
1

Client (Harvey)

AI agent that decides which tools to invoke based on user questions
2

MCP Server

Protocol server exposing tools and resources via JSON-RPC
3

Backend Services

Analysis API, A-MINT API, and CSP solvers that perform actual work

Key Design Decision

Client-Mediated Authentication

The MCP server does not hold API keys. Harvey (the client) provides credentials when launching the server. This aligns with MCP’s security model where clients control access.From the Harvey API README:
“No LLM or upstream API keys are required in the MCP server when used this way, aligning with MCP’s client‑mediated access model.”

MCP Components

MCP defines three types of resources that servers can expose:

1. Tools

Tools are executable functions that agents can invoke. Pricing Intelligence exposes five tools:

iPricing

Fetches or returns Pricing2Yaml documents

summary

Provides catalog-level statistics

subscriptions

Enumerates valid configurations

optimal

Finds best configuration by objective

validate

Validates pricing model consistency
Each tool:
  • Has a name (e.g., "optimal")
  • Accepts structured arguments (JSON schema)
  • Returns structured results (JSON content blocks)
  • Can raise errors (JSON-RPC error responses)

2. Resources

Resources are static data that agents can read. Pricing Intelligence exposes one resource:

resource://pricing/specification

The Pricing2Yaml specification markdown document. Harvey reads this when users ask about schema syntax or validation rules.URI: resource://pricing/specificationContent: Full Pricing2Yaml syntax specification

3. Prompts (Not Used)

MCP also supports prompts (reusable prompt templates), but Pricing Intelligence does not currently expose any. Harvey maintains its own planning and answer prompts internally.

Tool Invocation Flow

Here’s how Harvey invokes an MCP tool:
1

Harvey Decides

Based on the user’s question, Harvey plans to call optimal with specific filters
2

JSON-RPC Request

Harvey sends a tools/call request over stdio:
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "optimal",
    "arguments": {
      "pricing_url": "https://buffer.com/pricing",
      "filters": {"features": ["hashtagManager"]},
      "objective": "minimize",
      "solver": "minizinc"
    }
  }
}
3

MCP Server Processes

The MCP server:
  1. Validates the arguments
  2. Calls the internal workflow (container.workflow.run_optimal(...))
  3. The workflow fetches YAML (via A-MINT), invokes the Analysis API, and receives CSP results
4

JSON-RPC Response

The MCP server returns results:
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"optimal\": {\"subscription\": {\"plan\": \"ESSENTIALS\", \"addOns\": []}, \"cost\": 6.0}}"
      }
    ]
  }
}
5

Harvey Parses

Harvey extracts the JSON from the text content and uses it in answer generation
Why stdio? MCP supports multiple transports (HTTP, WebSocket, stdio). Pricing Intelligence uses stdio (standard input/output) for local server communication. Harvey spawns the MCP server as a subprocess and communicates via JSON-RPC over stdin/stdout.

MCP Server Implementation

The MCP server is implemented in Python using the official MCP SDK:
# From mcp_server/src/pricing_mcp/mcp_server.py:108-149

from mcp.server.fastmcp import FastMCP

mcp = FastMCP(
    settings.mcp_server_name,
    host=settings.http_host,
    port=settings.http_port,
)

@mcp.tool()
async def optimal(
    pricing_url: Optional[str] = None,
    pricing_yaml: Optional[str] = None,
    filters: Optional[Dict[str, Any]] = None,
    solver: str = "minizinc",
    objective: str = "minimize",
    refresh: bool = False,
) -> Dict[str, Any]:
    """Compute the optimal subscription under the provided constraints."""
    
    if not (pricing_url or pricing_yaml):
        raise ValueError(
            "optimal requires pricing_url or pricing_yaml to run analysis."
        )
    
    if solver not in VALID_SOLVERS:
        raise ValueError(INVALID_SOLVER_ERROR)
    
    if objective not in {"minimize", "maximize"}:
        raise ValueError("objective must be 'minimize' or 'maximize'.")
    
    logger.info(
        TOOL_INVOKED,
        tool="optimal",
        pricing_url=pricing_url,
        has_pricing_yaml=bool(pricing_yaml),
        filters=filters,
        solver=solver,
        objective=objective,
        refresh=refresh,
    )
    
    result = await container.workflow.run_optimal(
        url=pricing_url or "",
        filters=filters,
        solver=solver,
        objective=objective,
        refresh=refresh,
        yaml_content=pricing_yaml,
    )
    
    logger.info(TOOL_COMPLETED, tool="optimal", keys=list(result.keys()))
    return result

FastMCP SDK

Pricing Intelligence uses the fastmcp library, which provides:
  • Decorator-based tool registration: @mcp.tool()
  • Automatic JSON-RPC handling: Converts Python functions to MCP tools
  • Type validation: Uses Python type hints for argument validation
  • Multiple transports: Supports stdio, HTTP, WebSocket
  • Logging and observability: Structured logging for all tool invocations

Tool Signatures

Common Parameters

All tools accept:
pricing_url
string
URL of the SaaS pricing page (e.g., "https://buffer.com/pricing"). The server will fetch and parse the Pricing2Yaml model.
pricing_yaml
string
Direct Pricing2Yaml content as a string. Use this if you have the YAML already (e.g., from a file upload).
refresh
boolean
default:false
If true, bypass caches and fetch fresh data. Useful for development or when you know a pricing page has changed.
Exactly one of pricing_url or pricing_yaml must be provided. If both are given, pricing_yaml takes precedence.

Tool-Specific Parameters

filters
object
Filter criteria (see CSP Analysis for schema)
objective
enum
default:"minimize"
Optimization objective: "minimize" or "maximize"
solver
enum
default:"minizinc"
CSP solver: "minizinc" or "choco"
filters
object
Filter criteria
solver
enum
default:"minizinc"
CSP solver: "minizinc" or "choco"
solver
enum
default:"minizinc"
CSP solver: "minizinc" or "choco"
No additional parameters (only pricing_url, pricing_yaml, refresh)
No additional parameters (only pricing_url, pricing_yaml, refresh)

Workflow Orchestration

Internally, the MCP server delegates to a workflow layer that orchestrates backend services:
1

A-MINT Integration

The workflow calls A-MINT API to transform URLs into Pricing2Yaml:
yaml_content = await amint_client.transform(pricing_url)
2

Caching

Transformed YAML is cached (in-memory or Redis) to avoid redundant API calls
3

Analysis API Call

The workflow sends the YAML and filters to the Analysis API:
result = await analysis_client.optimal(
    pricing_yaml=yaml_content,
    filters=filters,
    solver=solver,
    objective=objective
)
4

Result Processing

The workflow formats the response and returns it to Harvey
# From mcp_server/src/pricing_mcp/workflows/pricing.py (conceptual)

class PricingWorkflow:
    def __init__(self, amint_client, analysis_client, cache):
        self.amint = amint_client
        self.analysis = analysis_client
        self.cache = cache
    
    async def run_optimal(
        self,
        url: Optional[str],
        yaml_content: Optional[str],
        filters: Optional[Dict],
        solver: str,
        objective: str,
        refresh: bool,
    ) -> Dict:
        # 1. Get Pricing2Yaml content
        if yaml_content:
            pricing_yaml = yaml_content
        elif url:
            cache_key = f"pricing:{url}"
            if not refresh:
                cached = await self.cache.get(cache_key)
                if cached:
                    pricing_yaml = cached
                else:
                    pricing_yaml = await self.amint.transform(url)
                    await self.cache.set(cache_key, pricing_yaml)
            else:
                pricing_yaml = await self.amint.transform(url)
                await self.cache.set(cache_key, pricing_yaml)
        else:
            raise ValueError("Must provide url or yaml_content")
        
        # 2. Call Analysis API
        result = await self.analysis.optimal(
            pricing_yaml=pricing_yaml,
            filters=filters,
            solver=solver,
            objective=objective,
        )
        
        # 3. Return structured result
        return result

Caching Strategy

The MCP server implements caching to reduce latency and API costs:

In-Memory Cache

Default: Simple Python dict for storing transformed YAML. Fast but lost on restart.

Redis Cache

Optional: Persistent cache shared across instances. Enable via CACHE_BACKEND=redis.
Cache Keys:
  • pricing:{url} → Full Pricing2Yaml content
  • summary:{url} → Summary statistics
  • validation:{url}:{solver} → Validation results
TTL (Time to Live):
  • Pricing YAML: 24 hours (pricing pages don’t change often)
  • Summaries: 24 hours
  • Validation: 24 hours
  • Analysis results: No caching (user-specific filters)
Use refresh=true in tool calls to bypass cache and force fresh data fetching.

Error Handling

MCP uses JSON-RPC error responses. The server translates Python exceptions:
# User calls optimal without url or yaml
raise ValueError(
    "optimal requires pricing_url or pricing_yaml to run analysis."
)

# MCP server responds:
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,  # Invalid params
    "message": "optimal requires pricing_url or pricing_yaml to run analysis."
  }
}
Common Error Codes:
  • -32600: Invalid Request
  • -32601: Method not found (unknown tool name)
  • -32602: Invalid params (bad arguments)
  • -32603: Internal error (server crash)
  • -32000 to -32099: Custom application errors

Configuration

MCP server behavior is controlled via environment variables:
MCP_SERVER_NAME
string
default:"Pricing Intelligence MCP Server"
Server name reported in initialize response
MCP_TRANSPORT
enum
default:"stdio"
Transport protocol: "stdio", "http", or "websocket"
HTTP_HOST
string
default:"0.0.0.0"
Bind address for HTTP/WebSocket transports
HTTP_PORT
number
default:8085
Port for HTTP/WebSocket transports
AMINT_BASE_URL
string
required
Base URL for A-MINT transformation API
ANALYSIS_BASE_URL
string
required
Base URL for Analysis API
CACHE_BACKEND
enum
default:"memory"
Cache implementation: "memory" or "redis"
REDIS_URL
string
Redis connection string (if CACHE_BACKEND=redis)
LOG_LEVEL
enum
default:"INFO"
Logging verbosity: "DEBUG", "INFO", "WARNING", "ERROR"

Observability

The MCP server emits structured logs for all operations:
{
  "timestamp": "2024-07-02T10:30:00Z",
  "level": "INFO",
  "event": "mcp.tool.invoked",
  "tool": "optimal",
  "pricing_url": "https://buffer.com/pricing",
  "filters": {"features": ["hashtagManager"]},
  "solver": "minizinc",
  "objective": "minimize"
}
These logs enable:
  • Performance monitoring: Track tool execution times
  • Usage analytics: See which tools are most popular
  • Debugging: Trace failures back to specific invocations
  • Auditing: Record all pricing analysis requests

Testing MCP Tools

You can test MCP tools directly using the mcp CLI:
pip install mcp

MCP vs Direct API Calls

Why use MCP instead of Harvey calling the Analysis API directly?

Abstraction

Harvey doesn’t need to know about A-MINT, Analysis API, or CSP solvers. It just calls MCP tools.

Introspection

MCP provides tools/list to discover available tools dynamically. Future agents can adapt without code changes.

Security

API keys for A-MINT and Analysis API are hidden from Harvey. Only the MCP server needs them.

Testability

You can test MCP tools independently of Harvey. Swap implementations without changing the agent.

Standardization

MCP is an open protocol. Future clients (other than Harvey) can use the same tools.

Observability

Centralized logging at the MCP layer provides a single audit trail.

Future Enhancements

Potential MCP features for Pricing Intelligence:
1

Resource Templates

Expose pricing models as resources:
resource://pricing/buffer/2024
resource://pricing/github/2023
Clients could browse available pricings.
2

Prompts

Expose reusable prompts:
prompt://pricing/compare-plans
prompt://pricing/optimize-cost
3

Streaming Results

For large enumeration, stream subscriptions incrementally:
{"subscription": 1, "cost": 10}
{"subscription": 2, "cost": 15}
...
4

Tool Composition

Tools that call other tools:
@mcp.tool()
async def compare(
    url_a: str,
    url_b: str,
    filters: Dict
):
    result_a = await optimal(url_a, filters=filters)
    result_b = await optimal(url_b, filters=filters)
    return {"comparison": {"a": result_a, "b": result_b}}

Further Reading

MCP Specification

Official MCP protocol documentation

FastMCP SDK

Python MCP server library

MCP Tools Reference

Pricing Intelligence MCP tools

Harvey Agent

How Harvey uses MCP

Build docs developers (and LLMs) love