Skip to content

Latest commit

 

History

History
447 lines (351 loc) · 14.7 KB

File metadata and controls

447 lines (351 loc) · 14.7 KB

Streamable HTTP Transport Setup

Overview

Meta Ads MCP supports Streamable HTTP Transport, which allows you to run the server as a standalone HTTP API. This enables direct integration with web applications, custom dashboards, and any system that can make HTTP requests.

Quick Start

1. Start the HTTP Server

# Basic HTTP server (default: localhost:8080)
python -m meta_ads_mcp --transport streamable-http

# Custom host and port
python -m meta_ads_mcp --transport streamable-http --host 0.0.0.0 --port 9000

2. Set Authentication

When you run this server yourself, the credential is a Meta access token from your own Meta app — create one at developers.facebook.com. A Pipeboard API token will not work here: the server passes whatever you give it straight to the Meta Graph API.

export META_ACCESS_TOKEN=your_meta_access_token

Setting this is optional for the HTTP transport if you pass the token per request in a header (see Security Model), but it is convenient for command-line use.

Want to authenticate with a Pipeboard API token instead, and never handle a Meta token? Use the hosted MCP at https://meta-ads.mcp.pipeboard.co/ rather than running this server. (PIPEBOARD_API_TOKEN is no longer supported by this package — see Migration.)

3. Make HTTP Requests

The server accepts JSON-RPC 2.0 requests at the /mcp endpoint. Use the Authorization header to provide your token.

curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer your_meta_access_token" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "id": 1,
    "params": {
      "name": "get_ad_accounts",
      "arguments": {"limit": 5}
    }
  }'

Configuration Options

Command Line Arguments

Argument Description Default
--transport Transport mode stdio
--host Server host address localhost
--port Server port 8080
--sse-response Return responses as SSE streams instead of JSON false (JSON)

Examples

# Local development server
python -m meta_ads_mcp --transport streamable-http --host localhost --port 8080

# Production server (accessible externally)
python -m meta_ads_mcp --transport streamable-http --host 0.0.0.0 --port 8080

# Custom port
python -m meta_ads_mcp --transport streamable-http --port 9000

Security Model (read before exposing the port)

The HTTP transport uses a per-request authentication model that is deliberately different from stdio. Understanding it is the difference between a safe deployment and leaking your Meta account to the internet.

  • stdio (local): the server acts as whatever credential is configured in its environment — META_ACCESS_TOKEN or a stored login from the local OAuth flow. Only the local process that launched it can talk to it, so this is expected and safe. If all you want is to use the MCP locally, prefer stdio.

  • streamable-http (network): every request must carry its own token header — Authorization: Bearer <token>, X-META-ACCESS-TOKEN, or the legacy X-PIPEBOARD-API-TOKEN. Requests without one are rejected with 401 Unauthorized. The server-side META_ACCESS_TOKEN environment variable is intentionally not used as an implicit fallback for HTTP requests — if it were, any caller who can reach the port would act as you. This gating is enforced by AuthInjectionMiddleware.

Operational rules:

  1. Do not expose the raw port to an untrusted network. --host 0.0.0.0 binds to every interface. Put the server behind an authenticating reverse proxy (or a private network / firewall), exactly as the hosted MCP at *.mcp.pipeboard.co does (localhost-bound Python process behind a proxy).
  2. Avoid setting META_ACCESS_TOKEN on a network-exposed HTTP server. It is an operator-wide, long-lived credential. Have callers pass their own tokens in headers instead. If you must set it (e.g. single-tenant behind a trusted proxy), ensure the port is unreachable by untrusted callers.
  3. The auth gate applies in both response modes — default JSON and --sse-response. (Historically --sse-response had a bug where the gate was not attached to the served app; see SECURITY.md, GHSA-8353-5qhw-8hfw. Fixed in 1.0.119 — upgrade if you use --sse-response.)
  4. If you ever exposed an unauthenticated server, rotate the Meta access token and review Graph API access logs.

Authentication

Which credential you use depends on who runs the server.

Self-hosted: Meta access token

When you run this package yourself, supply a Meta access token from your own Meta app. Create one at developers.facebook.com, then pass it in the Authorization header:

curl -H "Authorization: Bearer your_meta_access_token" \
     -X POST http://localhost:8080/mcp \
     -H "Content-Type: application/json" \
     -H "Accept: application/json, text/event-stream" \
     -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

The X-META-ACCESS-TOKEN header is equivalent:

curl -H "X-META-ACCESS-TOKEN: your_meta_access_token" \
     -X POST http://localhost:8080/mcp \
     -H "Content-Type: application/json" \
     -H "Accept: application/json, text/event-stream" \
     -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

Hosted: Pipeboard API token

The hosted Remote MCP at https://meta-ads.mcp.pipeboard.co/ accepts a Pipeboard API token instead, so you never handle a Meta token yourself. Pipeboard resolves the Meta credential server-side and enforces the API token's account and permission scoping on every call.

  1. Sign up at Pipeboard.co
  2. Generate an API token at pipeboard.co/api-tokens
  3. Point your MCP client at https://meta-ads.mcp.pipeboard.co/ with Authorization: Bearer <your_pipeboard_token>

For clients that cannot send headers, the token may be passed as a URL parameter:

https://meta-ads.mcp.pipeboard.co/?token=YOUR_PIPEBOARD_TOKEN

A Pipeboard API token only works against the hosted endpoint. Passing one to a server you run yourself will fail, because that server forwards the token directly to the Meta Graph API.

Available Endpoints

Server URL Structure

Base URL: http://localhost:8080
MCP Endpoint: /mcp

MCP Protocol Methods

Method Description
initialize Initialize MCP session and exchange capabilities
tools/list Get list of all available Meta Ads tools
tools/call Execute a specific tool with parameters

Response Format

All responses follow JSON-RPC 2.0 format:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    // Tool response data
  }
}

Example Usage

1. Initialize Session

curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer your_token" \
  -d '{
    "jsonrpc": "2.0",
    "method": "initialize",
    "id": 1,
    "params": {
      "protocolVersion": "2024-11-05",
      "capabilities": {"roots": {"listChanged": true}},
      "clientInfo": {"name": "my-app", "version": "1.0.0"}
    }
  }'

2. List Available Tools

curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer your_token" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/list",
    "id": 2
  }'

3. Get Ad Accounts

curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer your_token" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "id": 3,
    "params": {
      "name": "get_ad_accounts",
      "arguments": {"limit": 10}
    }
  }'

4. Get Campaign Performance

curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer your_token" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "id": 4,
    "params": {
      "name": "get_insights",
      "arguments": {
        "object_id": "act_701351919139047",
        "time_range": "last_30d",
        "level": "campaign"
      }
    }
  }'

Client Examples

Python Client

import requests
import json

class MetaAdsMCPClient:
    def __init__(self, base_url="http://localhost:8080", token=None):
        self.base_url = base_url
        self.endpoint = f"{base_url}/mcp"
        self.headers = {
            "Content-Type": "application/json",
            "Accept": "application/json, text/event-stream"
        }
        if token:
            self.headers["Authorization"] = f"Bearer {token}"
    
    def call_tool(self, tool_name, arguments=None):
        payload = {
            "jsonrpc": "2.0",
            "method": "tools/call",
            "id": 1,
            "params": {"name": tool_name}
        }
        if arguments:
            payload["params"]["arguments"] = arguments
        
        response = requests.post(self.endpoint, headers=self.headers, json=payload)
        return response.json()

# Usage
client = MetaAdsMCPClient(token="your_meta_access_token")
result = client.call_tool("get_ad_accounts", {"limit": 5})
print(json.dumps(result, indent=2))

JavaScript/Node.js Client

const axios = require('axios');

class MetaAdsMCPClient {
    constructor(baseUrl = 'http://localhost:8080', token = null) {
        this.baseUrl = baseUrl;
        this.endpoint = `${baseUrl}/mcp`;
        this.headers = {
            'Content-Type': 'application/json',
            'Accept': 'application/json, text/event-stream'
        };
        if (token) {
            this.headers['Authorization'] = `Bearer ${token}`;
        }
    }

    async callTool(toolName, arguments = null) {
        const payload = {
            jsonrpc: '2.0',
            method: 'tools/call',
            id: 1,
            params: { name: toolName }
        };
        if (arguments) {
            payload.params.arguments = arguments;
        }

        try {
            const response = await axios.post(this.endpoint, payload, { headers: this.headers });
            return response.data;
        } catch (error) {
            return { error: error.message };
        }
    }
}

// Usage
const client = new MetaAdsMCPClient('http://localhost:8080', 'your_meta_access_token');
client.callTool('get_ad_accounts', { limit: 5 })
    .then(result => console.log(JSON.stringify(result, null, 2)));

Production Deployment

Security Considerations

See Security Model above for the per-request auth model and why it matters. In short:

  1. Use HTTPS: In production, run behind a reverse proxy with SSL/TLS
  2. Authentication: Every request must carry its own token header; the server returns 401 otherwise. Do not rely on a server-side META_ACCESS_TOKEN as an implicit credential for HTTP callers — it is not used as a fallback.
  3. Network Security: Never expose the raw port to an untrusted network. Bind to localhost behind an authenticating proxy, or restrict with firewalls.
  4. Avoid META_ACCESS_TOKEN on network-exposed servers: it is an operator-wide credential; prefer per-request header tokens.
  5. Rate Limiting: Consider implementing rate limiting for public APIs

Docker Deployment

FROM python:3.10-slim

WORKDIR /app
COPY . .
RUN pip install -e .

EXPOSE 8080

CMD ["python", "-m", "meta_ads_mcp", "--transport", "streamable-http", "--host", "0.0.0.0", "--port", "8080"]

Environment Variables

# Meta access token from your own Meta app. Used for stdio; for HTTP it can
# also be passed per request in the Authorization header.
export META_ACCESS_TOKEN=your_meta_access_token

# Optional (for the local OAuth flow instead of a direct token)
export META_APP_ID=your_app_id
export META_APP_SECRET=your_app_secret

# Optional (for direct Meta token).
# WARNING: on the HTTP transport this is NOT used as a fallback credential for
# incoming requests — callers must pass their own token header (see "Security
# Model"). Setting it on a network-exposed server is an operator-wide credential
# risk; prefer stdio, or keep the port unreachable by untrusted callers.
export META_ACCESS_TOKEN=your_access_token

Troubleshooting

Common Issues

  1. Connection Refused: Ensure the server is running and accessible on the specified port.
  2. Authentication Failed: Verify your Bearer token is valid and included in the Authorization header.
  3. 404 Not Found: Make sure you're using the correct endpoint (/mcp).
  4. JSON-RPC Errors: Check that your request follows the JSON-RPC 2.0 format.

Debug Mode

Enable verbose logging by setting the log level in your environment if the application supports it, or check the application's logging configuration. The current implementation logs to a file.

Health Check

Test if the server is running by sending a tools/list request:

curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer your_token" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

Migration from stdio

If you're currently using stdio transport with MCP clients, you can support both stdio for local clients and HTTP for web applications. The application can only run in one mode at a time, so you may need to run two separate instances if you need both simultaneously.

  1. Keep existing MCP client setup (Claude Desktop, Cursor, etc.) using stdio.
  2. Add HTTP transport for web applications and custom integrations by running a separate server instance with the --transport streamable-http flag.
  3. Use the same authentication method:
    • For stdio, the META_ACCESS_TOKEN environment variable is used.
    • For HTTP, pass the token in the Authorization: Bearer <token> header.

Removed: PIPEBOARD_API_TOKEN used to exchange a Pipeboard API token for the underlying Meta access token. That exchange has been removed, because the Meta token it returned ignored the scoping on the API token that requested it. Either set META_ACCESS_TOKEN from your own Meta app, or use the hosted MCP at https://meta-ads.mcp.pipeboard.co/.

Both transports access the same Meta Ads functionality and use the same underlying authentication system.