Skip to content

Add remote MCP server for AI-assisted access to tracker data#117

Open
matthelm wants to merge 1 commit intoBuildCanada:mainfrom
matthelm:feat/mcp-server
Open

Add remote MCP server for AI-assisted access to tracker data#117
matthelm wants to merge 1 commit intoBuildCanada:mainfrom
matthelm:feat/mcp-server

Conversation

@matthelm
Copy link

Summary

This adds a Model Context Protocol (MCP) server so AI assistants can query Build Canada tracker data directly. Anyone using Claude Desktop, Claude Code, Cursor, or any MCP client can connect with a single URL:

https://www.buildcanada.com/mcp

In Claude Desktop: Settings → Connectors → Add custom connector → paste the URL.

What's included

12 read-only tools that mirror the existing REST API — no duplicated query logic. Each tool dispatches internally to the existing controllers via Rack, so when a controller or view changes, the MCP output updates automatically.

Tool REST endpoint What it does
list_commitments GET /commitments Search/filter commitments with pagination
get_commitment GET /commitments/:id Full commitment detail with criteria, sources, timeline
list_promises GET /promises All platform promises with progress scores
get_promise GET /promises/:id Promise detail with evidence links
list_bills GET /bills Parliamentary bills with stage dates
get_bill GET /bills/:id Bill detail with Parliament API data
list_departments GET /departments Departments with minister info
get_department GET /departments/:id Department detail with lead promises
list_ministers GET /ministers Cabinet officials by portfolio
list_activity GET /feed Chronological activity feed
get_commitment_summary GET /api/dashboard/:id/at_a_glance Status overview by policy area
get_commitment_progress GET /api/burndown/:id Time-series for trend analysis

Implementation

The entire MCP surface is a single controller (mcp_controller.rb) with a declarative config array. Adding a new tool is ~8 lines — just a name, path, description, and schema. The controller dynamically generates MCP::Tool subclasses and dispatches tool calls to existing endpoints via Rack::MockRequest.

Uses the official mcp Ruby SDK (v0.9.1) with stateless Streamable HTTP transport. No session management needed, works on multi-node deploys out of the box.

Testing

29 integration tests covering all 12 tools, filters, not-found paths, and protocol handshake.

To test locally with Claude Desktop:

# Start Rails
bin/rails server -p 3099

# Expose via ngrok
ngrok http 3099

# In Claude Desktop: Settings → Connectors → Add custom connector
# Enter: https://<your-ngrok-url>/mcp

# You may need to allow the ngrok host:
# config/initializers/allow_tunnel.rb (do not commit)
Rails.application.config.hosts.clear
# Run tests
bin/rails test test/controllers/mcp_controller_test.rb

The Build Canada tracker already makes government accountability data
publicly available through a REST API. This PR adds a Model Context
Protocol (MCP) server so AI assistants can query that data directly.

Anyone using Claude Desktop, Claude Code, Cursor, or any MCP client
can connect with a single URL: https://www.buildcanada.com/mcp

## MCP server

Uses the official `mcp` Ruby SDK with stateless Streamable HTTP
transport. Thirteen read-only tools, no auth required:

- list_policy_areas — discover valid policy area slugs
- list_commitments / get_commitment — search and inspect commitments
- list_promises / get_promise — platform promises with progress scores
- list_bills / get_bill — parliamentary bills with stage tracking
- list_departments / get_department — departments with ministers
- list_ministers — cabinet officials by portfolio
- list_activity — chronological feed of government activity
- get_commitment_summary — status overview by policy area
- get_commitment_progress — time-series for trend analysis

Each tool dispatches internally to the existing REST endpoints via
Rack — zero duplicated query or serialization logic. The entire MCP
surface is a single controller with a declarative config array.
Adding a new tool is ~8 lines.

No existing files are modified — this is purely additive (new
controller, new policy_areas endpoint, routes, and tests).

## Testing locally

1. Start Rails: `bin/rails server -p 3099`

2. Verify: `curl -X POST http://localhost:3099/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'`

3. To test with Claude Desktop, expose via ngrok:

       ngrok http 3099

4. In Claude Desktop: Settings > Connectors > Add custom connector,
   then enter the ngrok URL with /mcp appended.

   You may need to allow the ngrok host in Rails:

       # config/initializers/allow_tunnel.rb (do not commit)
       Rails.application.config.hosts.clear

5. Run tests: `bin/rails test test/controllers/mcp_controller_test.rb`
Copy link
Member

@xrendan xrendan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also update the docs and CLAUDE.md to mention these things.

The mcp server will be available at buildcanada.com/tracker/mcp instead of buildcanada.com/mcp.

properties: { id: { type: "integer", description: "The commitment ID" } },
required: [ "id" ]
},
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really not clear in the code (I need to clean things up), but commitments now replace promises:

list_promises
get_promise

Can you remove these?

path_template = config[:path]
path_params = path_template.scan(/:(\w+)/).flatten.map(&:to_sym)

klass = Class.new(MCP::Tool) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's slightly more code, but I'd rather this as their own models instead of the dynamic generation.

Can you make this POROs and put them under models/mcp_tools with a concern that has the shared logic here with the properties as methods/class methods on the tool classes directly?

    {
      name: "get_commitment_progress",
      path: "/api/burndown/:government_id",
      description: <<~DESC.strip,
        Commitment progress over time — daily time-series of how many commitments have been
        scoped, started, completed, or broken throughout a government's mandate. Useful for
        trend analysis and charting. Returns { date, scope, started, completed, broken } per
        day, plus mandate_start/end dates. Tracks COMMITMENTS (not promises).
        The government_id for the current Government of Canada is 1.
      DESC
      properties: {
        government_id: { type: "integer", description: "Government ID (1 = current Government of Canada)" },
        source_type: { type: "string", description: "Filter by source type" },
        policy_area_slug: { type: "string", description: "Filter by policy area slug" },
        department_slug: { type: "string", description: "Filter by lead department slug" }
      },
      required: [ "government_id" ]
    }

Becomes something like:

description= <<~DESC.strip,
        Commitment progress over time — daily time-series of how many commitments have been
        scoped, started, completed, or broken throughout a government's mandate. Useful for
        trend analysis and charting. Returns { date, scope, started, completed, broken } per
        day, plus mandate_start/end dates. Tracks COMMITMENTS (not promises).
        The government_id for the current Government of Canada is 1.
      DESC
...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants