使用说明
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_urlorweb_pagelink 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:
-
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}' -
Tool response format: Tools return JSON wrapped in MCP content format:
{ "result": { "content": [{ "type": "text", "text": "{ ...actual data... }" }] } } -
Error responses: Errors are also wrapped in content format with
isError: true:{ "result": { "content": [{ "type": "text", "text": "{\"error\": \"council_name is required\"}" }], "isError": true } } -
Check server logs: Azure Functions logs show all tool calls and errors:
# In local development func start # Watch the console output when making requests -
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
- Test tools/list to verify MCP server is responding
- Test list_available_councils to verify council configuration loaded
- Test list_committees with specific council to verify data access
- Check Azure Function logs for detailed error messages
- 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 instructionstools/list- Returns 8 available tools with JSON Schema validationtools/call- Executes a tool and returns resultsping- 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=devto reinstall production-only dependencies intonode_modulesbefore packaging. - Deploys with
scm-do-build-during-deployment: false— Kudu does not attempt its own build; the uploaded package is already complete. .funcignoredoes not excludenode_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
- Council Identifiers Reference - Complete guide to valid council names
- Democratic Data Integrity - Guidelines for handling official records
License
MIT
