格洛斯特郡议会数据 MCP 服务

一个 MCP 服务器,通过 ModernGov SOAP API 统一访问格洛斯特郡全部 7 个议会的民主数据,包括委员会、议员、会议及会议文件。

  • by Gloucester-City-Council
  • JavaScript
  • 本地部署
  • 更新于
在 GitHub 查看 Gloucester-City-Council/GCC-MCP-Pilot

使用说明

Gloucestershire Councils MCP Server

Azure Functions v4 MCP (Model Context Protocol) server for accessing democratic data from all 7 Gloucestershire councils through their ModernGov SOAP APIs.

Current Status

Component Status Notes
MCP protocol implementation ✅ Complete initialize, tools/list, tools/call
Multi-council support ✅ Complete All 7 Gloucestershire councils
Tool schemas defined ✅ Complete 8 tools with full JSON Schema & enum validation
Tool routing ✅ Complete End-to-end flow working
Council discovery ✅ Complete list_available_councils tool
ModernGov SOAP client ✅ Complete Multi-endpoint support with fallback to knowledge base
Knowledge base ✅ Complete 117 committees, 295 wards across all councils

Supported Councils (7)

Council Committees Wards URL
Gloucestershire County Council 32 55 https://glostext.gloucestershire.gov.uk
Gloucester City Council 15 39 https://democracy.gloucester.gov.uk
Tewkesbury Borough Council 15 38 https://minutes.tewkesbury.gov.uk
Stroud District Council 10 51 https://stroud.moderngov.co.uk
Cheltenham Borough Council 18 40 https://democracy.cheltenham.gov.uk
Cotswold District Council 18 34 https://meetings.cotswold.gov.uk
Forest of Dean District Council 9 38 https://meetings.fdean.gov.uk

See docs/COUNCIL_IDENTIFIERS.md for exact council name strings required in API calls.

Project Structure

GCC-MCP-Pilot/
├── package.json
├── host.json
├── src/
│   ├── index.js                    # Azure Functions entry point
│   └── functions/
│       ├── mcp.js                  # Main MCP HTTP endpoint
│       └── test-soap.js            # SOAP exploration endpoint
├── lib/
│   ├── mcp-handler.js              # MCP JSON-RPC protocol handler
│   ├── moderngov-client.js         # SOAP client with multi-council support
│   ├── council-config.js           # Council configuration loader
│   └── tools/
│       ├── list-committees.js
│       ├── get-councillors.js
│       ├── get-councillors-by-ward.js
│       ├── get-meetings.js
│       ├── get-meeting-details.js
│       ├── get-attachment.js
│       └── analyze-meeting-document.js
├── json/
│   └── Gloucestershire/
│       ├── councils.json           # Council endpoints
│       └── council_data/
│           ├── Gloucester_City_Council/
│           │   ├── committees.json
│           │   └── wards.json
│           ├── Tewkesbury_Borough_Council/
│           │   ├── committees.json
│           │   └── wards.json
│           └── ... (5 more councils)
└── docs/
    ├── COUNCIL_IDENTIFIERS.md      # Council reference guide
    └── DEMOCRATIC_DATA_INTEGRITY.md

Available MCP Tools

Tool Parameters Description
list_available_councils None Start here! Lists all 7 councils with metadata
list_committees council_name (optional) List committees for one or all councils
get_councillors council_name (required) Get all councillors by ward for a council
get_councillors_by_ward council_name, ward_name Get councillors for a specific ward
get_meetings council_name, committee_id Get meetings for a committee
get_meeting_details council_name, meeting_id Get detailed meeting information
get_attachment council_name, attachment_id Get document metadata and URL
analyze_meeting_document url Extract structured content from PDFs

Required Parameters

IMPORTANT: Most tools require council_name parameter with an exact case-sensitive match:

✅ Correct: "Gloucester City Council" ❌ Wrong: "gloucester city council" (case mismatch) ❌ Wrong: "Gloucester Council" (missing "City") ❌ Wrong: "GCC" (abbreviation)

Use list_available_councils first to get the exact council names!

Shields badge MCP

A separate endpoint that builds Shields.io badges. Hand-written Shields URLs break on dashes, underscores and spaces, and a badge that shows a number has to be edited by hand every time the number changes. This MCP does the escaping and adds live badges, which change without anyone editing the page that embeds them.

Route Auth Purpose
POST /api/mcp-shields Function key MCP JSON-RPC endpoint. Can create and change badges.
GET /api/mcp-shields/badge/{key} Anonymous Read-only JSON that Shields.io fetches to draw a live badge.

Tools: badge_static, badge_threshold, badge_review_stamp, badge_wall, badge_set, badge_list, badge_delete. Every embed uses label: message as alt text.

badge_review_stamp writes dates out in full, for example 16 Apr 2027. A due stamp is green until warnDays (default 30) before the date, orange inside that window, and red once the date has passed.

Live badges

badge_set stores a label, message and colour under a key. The embed URL points Shields at the badge route, so the URL never changes while the value does. Shields caches for a few minutes, so a change is not instant.

A live badge can also carry its own look: logo (a Simple Icons slug such as bat), logoColor, labelColor and style. These are stored with the badge and served in the JSON, so anyone who embeds the plain URL gets the same badge. Without them a badge is the default flat style with two boxes. Setting a key again replaces the whole badge, including its look, so repeat the look fields on every update.

Values live in Blob Storage, one blob per badge at badges/<key>.json. The function reuses STORAGE_CONNECTION, the same setting mcp-notes uses, and creates the container on first use.

The badge route is anonymous because Shields has to reach it. Anyone who knows or guesses a key can read that badge's label, message, colours, logo name and style. Keys are not secret. Do not put anything in a badge that you would not put on a public page.

Variable Description Default
STORAGE_CONNECTION Blob Storage connection string. Already set for mcp-notes. None, required for live badges
MCP_SHIELDS_CONTAINER Container for live badges. mcp-shields
SHIELDS_PUBLIC_BASE_URL Origin used in embed URLs. https://$WEBSITE_HOSTNAME

The static tools need none of these.

# Create a live badge
curl -X POST "http://localhost:7071/api/mcp-shields?code=<function-key>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"badge_set","arguments":{"key":"lint-pass-rate","label":"lint pass rate","value":94,"suffix":"%","logo":"github","labelColor":"24292f","style":"for-the-badge"}},"id":1}'

# Read what Shields will read
curl http://localhost:7071/api/mcp-shields/badge/lint-pass-rate

Locally, func start has no function key check, so ?code= is only needed once deployed. Get the key from Portal: Function App, Functions, mcpShields, Function Keys.

Democratic Data Integrity

IMPORTANT: This MCP server returns official statutory records of democratic decision-making. AI assistants using this data must follow special handling rules:

  • Quote verbatim: Committee recommendations, decisions, resolutions, and motions must be quoted exactly
  • Always link sources: Include the source_url or web_page link from responses
  • Separate interpretation: Clearly distinguish official record from explanations

See docs/DEMOCRATIC_DATA_INTEGRITY.md for complete guidelines.

Data Classification Fields

Tool responses include metadata to identify official content:

{
  "data_classification": "official_record",
  "is_official_record": true,
  "official_sections": ["recommendations", "decisions", "legal_implications"],
  "source": {
    "system": "ModernGov",
    "council": "Gloucester City Council",
    "url": "https://democracy.gloucester.gov.uk/..."
  }
}

See docs/NOTES_BLOB_CLIENT_PERFORMANCE.md for blob client reuse and latency guidance for mcp-notes.

Local Development

Prerequisites

  • Node.js 18+
  • Azure Functions Core Tools v4

Setup

cd GCC-MCP-Pilot
npm install

Start the server

func start

The server will start at http://localhost:7071

Testing

Test MCP Protocol

# Test initialize - check server info and available councils
curl -X POST http://localhost:7071/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}'

# Test tools/list - see all available tools
curl -X POST http://localhost:7071/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":2}'

# Test list_available_councils - discover councils
curl -X POST http://localhost:7071/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list_available_councils","arguments":{}},"id":3}'

# Test list_committees - get committees for a specific council
curl -X POST http://localhost:7071/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list_committees","arguments":{"council_name":"Gloucester City Council"}},"id":4}'

# Test list_committees - get all councils' committees
curl -X POST http://localhost:7071/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list_committees","arguments":{}},"id":5}'

# Test get_councillors
curl -X POST http://localhost:7071/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"get_councillors","arguments":{"council_name":"Gloucester City Council"}},"id":6}'

Explore SOAP Responses

Use the test-soap endpoint to see actual XML responses from ModernGov:

# List available operations
curl http://localhost:7071/api/test-soap

# Test GetCommittees
curl http://localhost:7071/api/test-soap/GetCommittees

# Test GetCouncillorsByWard
curl http://localhost:7071/api/test-soap/GetCouncillorsByWard

# Test GetMeetings with committee ID
curl "http://localhost:7071/api/test-soap/GetMeetings?committeeId=544"

# Test GetMeeting with meeting ID
curl "http://localhost:7071/api/test-soap/GetMeeting?meetingId=123456"

Troubleshooting

"Not getting any information" from ChatGPT/Claude

If you're seeing errors like "not getting any information", check:

  1. Council name parameter: Ensure exact case-sensitive match

    # Check available councils first
    curl -X POST http://localhost:7071/api/mcp \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list_available_councils","arguments":{}},"id":1}'
    
  2. Tool response format: Tools return JSON wrapped in MCP content format:

    {
      "result": {
        "content": [{
          "type": "text",
          "text": "{ ...actual data... }"
        }]
      }
    }
    
  3. Error responses: Errors are also wrapped in content format with isError: true:

    {
      "result": {
        "content": [{
          "type": "text",
          "text": "{\"error\": \"council_name is required\"}"
        }],
        "isError": true
      }
    }
    
  4. Check server logs: Azure Functions logs show all tool calls and errors:

    # In local development
    func start
    # Watch the console output when making requests
    
  5. Test manually: Use curl commands above to verify tools work correctly

Common Errors

Error Cause Solution
council_name is required Missing parameter Add council_name parameter
Unknown council: ... Invalid council name Use list_available_councils to get exact names
Ward not found Invalid ward name Use get_councillors to see available wards
SOAP request failed: 403 Network restriction Normal - falls back to knowledge base data

Debugging Steps

  1. Test tools/list to verify MCP server is responding
  2. Test list_available_councils to verify council configuration loaded
  3. Test list_committees with specific council to verify data access
  4. Check Azure Function logs for detailed error messages
  5. Verify MCP client configuration in ChatGPT/Claude settings

MCP Client Configuration

ChatGPT Configuration

In ChatGPT settings, configure the MCP server:

{
  "mcpServers": {
    "gloucestershire-councils": {
      "url": "https://your-function-app.azurewebsites.net/api/mcp",
      "apiKey": "your-api-key-if-needed"
    }
  }
}

Claude Desktop Configuration

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "gloucestershire-councils": {
      "command": "node",
      "args": ["/path/to/GCC-MCP-Pilot/mcp-stdio-wrapper.js"],
      "env": {
        "MODERNGOV_ENDPOINT": "https://democracy.gloucester.gov.uk/mgWebService.asmx"
      }
    }
  }
}

Environment Variables

Variable Description Default
MODERNGOV_ENDPOINT ⚠️ Deprecated - now loaded from council config N/A

Council endpoints are now configured in json/Gloucestershire/councils.json.

MCP Protocol Reference

This server implements the Model Context Protocol (MCP) JSON-RPC interface:

Methods

  • initialize - Returns server capabilities, council list, and instructions
  • tools/list - Returns 8 available tools with JSON Schema validation
  • tools/call - Executes a tool and returns results
  • ping - Health check

Response Format

All responses follow JSON-RPC 2.0 format:

{
  "jsonrpc": "2.0",
  "result": { ... },
  "id": 1
}

Tool call results are wrapped in MCP content format:

{
  "jsonrpc": "2.0",
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{ ...JSON result with date context wrapper... }"
      }
    ]
  },
  "id": 1
}

Deployment

CI deploys to the func-mpc-poc Function App on every push to main via .github/workflows/main_func-mpc-poc.yml (Azure/functions-action@v1, publish-profile zip deploy).

Run-From-Package (read-only deployment)

func-mpc-poc runs with Run-From-Package: the Function App's WEBSITE_RUN_FROM_PACKAGE application setting is 1, so each deploy is sealed into an immutable, read-only package instead of being extracted into a writable wwwroot.

This requires the deployed zip to be fully self-contained — node_modules included, built before the zip is uploaded. With WEBSITE_RUN_FROM_PACKAGE=1, the uploaded zip becomes the final package as-is; there is no reliable post-deploy Kudu/Oryx build step to populate node_modules afterwards. (An earlier version of this doc claimed Oryx still builds server-side under Run-From-Package — that was wrong, and shipping a node_modules-less zip under this setting produced a fully broken deploy: every function failed to load, since even @azure/functions itself couldn't be found.) Accordingly, the workflow:

  • Runs npm ci && npm test (full install, including devDependencies, so tests can run).
  • Then runs npm ci --omit=dev to reinstall production-only dependencies into node_modules before packaging.
  • Deploys with scm-do-build-during-deployment: false — Kudu does not attempt its own build; the uploaded package is already complete.
  • .funcignore does not exclude node_modules/ — it's a required part of the deployed package now, not something Azure builds for you.

Operationally this means:

  • Deployments are atomic — a deploy either fully replaces the running package or doesn't take effect, no partially-extracted state.
  • The runtime filesystem is read-only. No function in this repo writes to local disk, so this is a safe default; if a future tool ever needs scratch space, use %TEMP%/os.tmpdir(), which remains writable.

If func-mpc-poc is ever recreated, re-apply this setting once:

az functionapp config appsettings set --name func-mpc-poc --resource-group <resource-group> --settings WEBSITE_RUN_FROM_PACKAGE=1

(or Portal: Function App → Configuration → Application settings → add WEBSITE_RUN_FROM_PACKAGE = 1 → Save.) Also make sure AzureWebJobsFeatureFlags=EnableWorkerIndexing is set — required for this repo's Node.js v4 programming model (code-based app.http(...) registration, used by every function here) to be indexed at all; without it the Functions host silently lists zero functions. Neither of these settings is something this repo's code or CI can apply on your behalf.

Manual publish (local fallback)

For an ad-hoc deploy from a developer machine, outside CI:

func azure functionapp publish <app-name>

Documentation

License

MIT