@winning-ground/campaign-mcp
v0.9.0
Published
MCP server giving AI assistants authorized access to Winning Ground campaign operations (staging and production)
Downloads
1,859
Readme
Campaign MCP
MCP server for the Campaign app (campaign-api / campaign-ui). Bots use this to read campaign health, manage campaign users, view crew, and do UI-parity CRM on constituents and contacts.
This is not Orchestrator MCP. Orchestrator MCP (apps/orchestrator-mcp) is staging-only and includes danger-zone deprovision — see that package's bot guide. Campaign MCP supports staging and production and has no reset/deprovision/scrub tools.
Recon found no existing Campaign MCP package. This app is a sibling of apps/orchestrator-mcp and talks only to campaign-api.
Auth and tenant model
HTTP /mcp uses OAuth 2.1 against auth-service. The user signs in in a browser; Campaign MCP then forwards that user JWT to campaign-api. Static wgk_* API keys are not used.
| Header | Value |
| --- | --- |
| Authorization | Bearer <user JWT> issued by auth-service for the campaign MCP resource |
| x-wg-tenant-host | Tenant hostname, e.g. acme.winningground.ca |
Do not send X-WG-Tenant-Id. Tenant scope comes from a known, active tenant domain plus the signed-in user's membership.
The MCP process also refuses hosts outside CAMPAIGN_MCP_TENANT_HOST + optional CAMPAIGN_MCP_ALLOWED_TENANT_HOSTS before it calls the API.
Unknown or inactive tenant hosts return 403 from campaign-api. Missing users:* / constituents:* grants return 403 from requirePermission — the same gates as Campaign UI.
Unauthenticated /mcp requests return 401 with WWW-Authenticate pointing at /.well-known/oauth-protected-resource.
Environments
| CAMPAIGN_MCP_ENV | Default CAMPAIGN_API_URL | Tenant host suffix |
| --- | --- | --- |
| local | http://localhost:4000 | *.winningground.local |
| staging | https://campaign-api.winningground.xyz | *.winningground.xyz |
| production | https://campaign.winningground.ca | *.winningground.ca |
CAMPAIGN_API_URL overrides the preset. prod is accepted as an alias for production. Do not reuse staging keys in production.
Required env
CAMPAIGN_MCP_ENV=staging
CAMPAIGN_MCP_TENANT_HOST=acme.winningground.xyz
# AUTH_SERVICE_URL=https://auth.winningground.xyz
# Optional extras (comma-separated). The bound tenant host is always allowed.
# CAMPAIGN_MCP_ALLOWED_TENANT_HOSTS=acme.winningground.xyz,beta.winningground.xyz
# CAMPAIGN_API_URL=https://campaign-api.winningground.xyz
# CAMPAIGN_MCP_PORT=4070
# CAMPAIGN_MCP_BIND=127.0.0.1This process verifies tokens minted for ${AUTH_SERVICE_URL}/mcp/campaign and, locally, http://127.0.0.1:4070/mcp / http://localhost:4070/mcp.
campaign-api session routes (/api/v1/account, /dashboard, …) accept only campaign-api / mobile-campaign-api. MCP-audience and :4070 loopback tokens get 401 there. campaign-api /api/v1/mcp/* accepts the campaign MCP resource (and, when the API is local, CAMPAIGN_ALLOW_LOOPBACK_AUDIENCE=1 also allows :4070). Set that flag in the root .env for local campaign-api — never in staging or production.
OAuth resource identifiers stay canonical (${AUTH_SERVICE_URL}/mcp/campaign). Hosted MCP URLs are not advertised as the resource; allowlist extras on auth-service with MCP_OAUTH_EXTRA_RESOURCES if a client must register a different resource.
Connect a bot
HTTP OAuth is the only supported transport. Stdio API-key mode is removed.
npx @winning-ground/campaign-mcp
# or, from this repo:
CAMPAIGN_MCP_ENV=staging \
CAMPAIGN_MCP_TENANT_HOST=acme.winningground.xyz \
pnpm --filter @winning-ground/campaign-mcp dev
# health: http://127.0.0.1:4070/health
# MCP: http://127.0.0.1:4070/mcpCursor / Claude Desktop
{
"mcpServers": {
"campaign-staging": {
"url": "http://127.0.0.1:4070/mcp"
}
}
}The client discovers auth-service from /.well-known/oauth-protected-resource, registers (DCR), and opens a browser for Winning Ground sign-in. Start with list_campaign_mcp_tools, then get_connection. If connection fails, check the tenant host and that the signed-in user can access that tenant.
Tool catalogue
Call list_campaign_mcp_tools for the machine-readable list. Summary:
| Tool | campaign-api | Ticket |
| --- | --- | --- |
| get_dashboard_insights | GET /api/v1/dashboard | WG-1270 |
| list_campaign_users | GET /api/v1/users | WG-1271 |
| list_user_invites | GET /api/v1/users/invites | WG-1271 |
| check_user_email | GET /api/v1/users/check-email | WG-1271 |
| invite_campaign_user | POST /api/v1/users/invite | WG-1271 |
| update_campaign_user | PATCH /api/v1/users/:id | WG-1271 |
| resend_user_invite | POST /api/v1/users/invites/:id/resend | WG-1271 |
| cancel_user_invite | DELETE /api/v1/users/invites/:id | WG-1271 |
| list_crew / get_crew_member / update_crew_member | /api/v1/crew | WG-1272 |
| list_crew_notes / add_crew_note | /api/v1/crew/:id/notes | WG-1272 |
| list_crew_skills / add_crew_skill | /api/v1/crew/:id/skills | WG-1272 |
| list_crew_activities / confirm_crew_activity | /api/v1/crew/:id/activities | WG-1272 |
| list_constituents / search_constituents / filter_constituents / get_constituent | /api/v1/constituents | WG-1273 |
| list_constituent_interactions / log_constituent_interaction | /api/v1/constituents/:id/interactions | WG-1273 |
| list_contacts / search_contacts / get_contact / create_contact / update_contact | /api/v1/contacts | WG-1273 |
UA / PostHog stays in campaign-ui. Insights start from /dashboard only.
Excluded ops (not exposed)
These exist on campaign-api / Campaign UI but are not MCP tools:
- Orchestrator danger-zone: deprovision, demo recreate, tenant scrub
POST /api/v1/crew/invite(creates a login + password-reset email; useinvite_campaign_user)- Contact import (
/contacts/import) - Contact ↔ constituent relink (
PUT /contacts/:id/constituent) - Contact/address/point deletes
- Lawn-sign requests from
POST /contacts/PUT /contacts/:id(interaction sign requests remain optional onlog_constituent_interaction) - Unscoped PostHog queries
Docs for bots
See AGENTS.md for the short agent-facing connection notes (WG-1269).
