Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,5 @@ gem "marksmith", "~> 0.4.5"
gem "commonmarker", "~> 2.3"

gem "appsignal"

gem "mcp"
6 changes: 6 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ GEM
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.12.2)
json-schema (6.2.0)
addressable (~> 2.8)
bigdecimal (>= 3.1, < 5)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
llhttp-ffi (0.5.1)
Expand All @@ -238,6 +241,8 @@ GEM
marcel (1.0.4)
marksmith (0.4.5)
activesupport
mcp (0.9.1)
json-schema (>= 4.1)
meta-tags (2.22.1)
actionpack (>= 6.0.0, < 8.1)
method_source (1.1.0)
Expand Down Expand Up @@ -456,6 +461,7 @@ DEPENDENCIES
importmap-rails (~> 2.1)
jbuilder
marksmith (~> 0.4.5)
mcp
nokogiri (~> 1.18)
pdf-reader (~> 2.12)
pg (~> 1.1)
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,13 @@ For new developers joining the project, we provide a streamlined onboarding proc
- Production database is automatically dumped weekly (every Monday at 2 AM UTC)
- Dumps are stored as GitHub Actions artifacts for 30 days
- Dumps use PostgreSQL's custom archive format for efficient storage and restore

### MCP Server

The application exposes a [Model Context Protocol](https://modelcontextprotocol.io/) endpoint
for AI agent integration at `https://www.buildcanada.com/tracker/mcp`.

This provides 10 read-only tools covering commitments, bills, departments, ministers, the
activity feed, and dashboard summaries. Tool classes are POROs under `app/models/mcp_tools/`.

To connect from Claude Desktop: Settings > Connectors > Add custom connector > paste the URL.
7 changes: 7 additions & 0 deletions agent/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

You are the Build Canada commitment evaluation agent. Do NOT search the filesystem for project structure — everything you need is documented here.

## MCP Server

The application exposes an MCP (Model Context Protocol) server at `https://www.buildcanada.com/tracker/mcp` (POST).
Available read-only tools: `list_commitments`, `get_commitment`, `list_bills`, `get_bill`, `list_departments`,
`get_department`, `list_ministers`, `list_activity`, `get_commitment_summary`, `get_commitment_progress`.
These tools proxy to the existing REST API endpoints and return JSON. Tool classes live under `app/models/mcp_tools/`.

## Rails API Reference

Base URL: provided in system prompt. Auth: `Authorization: Bearer <key>` (also in system prompt).
Expand Down
24 changes: 24 additions & 0 deletions app/controllers/mcp_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class McpController < ActionController::API
TOOLS = [
McpTools::ListCommitments,
McpTools::GetCommitment,
McpTools::ListBills,
McpTools::GetBill,
McpTools::ListDepartments,
McpTools::GetDepartment,
McpTools::ListMinisters,
McpTools::ListActivity,
McpTools::GetCommitmentSummary,
McpTools::GetCommitmentProgress
].freeze

def create
server = MCP::Server.new(name: "build-canada-tracker", version: "1.0.0", tools: TOOLS)
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
server.transport = transport

resp_status, resp_headers, resp_body = transport.handle_request(request)
resp_headers&.each { |key, value| response.headers[key] = value }
render json: resp_body&.first, status: resp_status
end
end
40 changes: 40 additions & 0 deletions app/models/concerns/mcp_rack_tool.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module McpRackTool
extend ActiveSupport::Concern

class_methods do
def path_template(value = nil)
if value
@path_template = value
@path_params = value.scan(/:(\w+)/).flatten.map(&:to_sym)
else
@path_template
end
end

def path_params
@path_params || []
end

def call(server_context:, **params)
path = path_template.gsub(/:(\w+)/) { params[$1.to_sym] }
query_params = params.except(*path_params)
response = rack_get(path, query_params)
MCP::Tool::Response.new([{ type: "text", text: response }])
end

private

def rack_get(path, params = {})
query_string = params.compact.to_query
url = query_string.empty? ? path : "#{path}?#{query_string}"

env = Rack::MockRequest.env_for(url, "REQUEST_METHOD" => "GET", "HTTP_ACCEPT" => "application/json", "HTTP_HOST" => "localhost")
status, headers, body = Rails.application.call(env)

chunks = []
body.each { |chunk| chunks << chunk }
body.close if body.respond_to?(:close)
chunks.join
end
end
end
16 changes: 16 additions & 0 deletions app/models/mcp_tools/get_bill.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class McpTools::GetBill < MCP::Tool
include McpRackTool

description <<~DESC.strip
Get full details for a parliamentary bill. Returns all fields including stage
dates and the complete raw data from the Parliament of Canada API (sponsor,
type, session info).
DESC

input_schema(
properties: { id: { type: "integer", description: "The bill database ID" } },
required: [ "id" ]
)

path_template "/bills/:id"
end
17 changes: 17 additions & 0 deletions app/models/mcp_tools/get_commitment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class McpTools::GetCommitment < MCP::Tool
include McpRackTool

description <<~DESC.strip
Get full details for a single commitment. Returns all nested data: sources
(original government documents), criteria (completion/success/progress/failure
with assessment history), departments, timeline events, status_history,
and recent_feed items.
DESC

input_schema(
properties: { id: { type: "integer", description: "The commitment ID" } },
required: [ "id" ]
)

path_template "/commitments/:id"
end
23 changes: 23 additions & 0 deletions app/models/mcp_tools/get_commitment_progress.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class McpTools::GetCommitmentProgress < MCP::Tool
include McpRackTool

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

input_schema(
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" ]
)

path_template "/api/burndown/:government_id"
end
20 changes: 20 additions & 0 deletions app/models/mcp_tools/get_commitment_summary.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class McpTools::GetCommitmentSummary < MCP::Tool
include McpRackTool

description <<~DESC.strip
Overall commitment status summary — how many commitments are not started, in progress,
completed, or broken, broken down by policy area. This aggregates COMMITMENTS (not
promises). If no commitments exist yet, totals will be zero even if promises are loaded.
The government_id for the current Government of Canada is 1.
DESC

input_schema(
properties: {
government_id: { type: "integer", description: "Government ID (1 = current Government of Canada)" },
source_type: { type: "string", description: "Filter by source type (e.g. 'platform_document', 'mandate_letter')" }
},
required: [ "government_id" ]
)

path_template "/api/dashboard/:government_id/at_a_glance"
end
16 changes: 16 additions & 0 deletions app/models/mcp_tools/get_department.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class McpTools::GetDepartment < MCP::Tool
include McpRackTool

description <<~DESC.strip
Get department details including minister info (hill office, constituency offices)
and the department's lead commitments. Accepts numeric ID or
slug (e.g. 'finance-canada', 'national-defence').
DESC

input_schema(
properties: { id_or_slug: { type: "string", description: "Department ID or slug" } },
required: [ "id_or_slug" ]
)

path_template "/departments/:id_or_slug"
end
25 changes: 25 additions & 0 deletions app/models/mcp_tools/list_activity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class McpTools::ListActivity < MCP::Tool
include McpRackTool

description <<~DESC.strip
Chronological feed of government activity on tracked commitments. Best tool for
"what's happening" or "what changed recently." Returns: event_type, title, summary,
occurred_at, linked commitment, and policy_area. Paginated, most recent first.
Note: only populated when the evaluation agent has assessed commitments and
created events — will be empty if no commitments have been evaluated yet.
DESC

input_schema(
properties: {
commitment_id: { type: "integer", description: "Filter to one commitment's activity" },
event_type: { type: "string", description: "Filter by event type" },
policy_area_id: { type: "integer", description: "Filter by policy area ID" },
since: { type: "string", description: "Activity after this date (ISO 8601, e.g. '2025-06-01')" },
until: { type: "string", description: "Activity before this date (ISO 8601)" },
page: { type: "integer", description: "Page number (default: 1)" },
per_page: { type: "integer", description: "Results per page, max 100 (default: 50)" }
}
)

path_template "/feed"
end
12 changes: 12 additions & 0 deletions app/models/mcp_tools/list_bills.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class McpTools::ListBills < MCP::Tool
include McpRackTool

description <<~DESC.strip
List Canadian parliamentary bills (45th Parliament). Returns bill_number_formatted
(e.g. 'C-2', 'S-201'), short_title, long_title, latest_activity, and all stage
dates tracking progress through Parliament: House 1st/2nd/3rd reading, Senate
1st/2nd/3rd reading, and Royal Assent.
DESC

path_template "/bills"
end
29 changes: 29 additions & 0 deletions app/models/mcp_tools/list_commitments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class McpTools::ListCommitments < MCP::Tool
include McpRackTool

description <<~DESC.strip
Search and filter government commitments — the core accountability tracking unit.
The Build Canada data model: Promises (raw political pledges) → Commitments
(specific, measurable outcomes with status tracking) → Evidence (bills, events, sources).
Each commitment has a status (not_started/in_progress/completed/broken), a type,
a policy area, and a lead department. Returns paginated results with
meta { total_count, page, per_page }.
Use get_commitment_summary to discover valid policy area slugs.
DESC

input_schema(
properties: {
q: { type: "string", description: "Full-text search on title and description" },
status: { type: "string", enum: %w[not_started in_progress completed broken], description: "Filter by commitment status" },
policy_area: { type: "string", description: "Policy area slug (e.g. 'defence', 'healthcare', 'economy')" },
commitment_type: { type: "string", enum: %w[legislative spending procedural institutional diplomatic aspirational outcome], description: "Filter by commitment type" },
department: { type: "string", description: "Department slug (e.g. 'finance-canada')" },
sort: { type: "string", enum: %w[title date_promised last_assessed_at status], description: "Sort field (default: created_at desc)" },
direction: { type: "string", enum: %w[asc desc], description: "Sort direction (default: desc)" },
page: { type: "integer", description: "Page number (default: 1)" },
per_page: { type: "integer", description: "Results per page, max 1000 (default: 50)" }
}
)

path_template "/commitments"
end
11 changes: 11 additions & 0 deletions app/models/mcp_tools/list_departments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class McpTools::ListDepartments < MCP::Tool
include McpRackTool

description <<~DESC.strip
List all ~32 federal government departments. Returns: id, display_name, slug,
official_name, priority, and minister info (name, title, contact details,
hill office) if a minister is assigned.
DESC

path_template "/departments"
end
10 changes: 10 additions & 0 deletions app/models/mcp_tools/list_ministers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class McpTools::ListMinisters < MCP::Tool
include McpRackTool

description <<~DESC.strip
List current cabinet ministers and officials. Returns: name, title, avatar_url,
email, phone, website, constituency, province, and their department assignment.
DESC

path_template "/ministers"
end
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,7 @@
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check

post "/mcp", to: "mcp#create"

root "application#root"
end
Loading
Loading