Skip to main content
Glama
dragosh29

Citizen Space MCP server

by dragosh29

Citizen Space MCP server

An MCP server that lets Claude, ChatGPT and other MCP clients work with a Citizen Space consultation site: the published consultations anyone can see, and, with a Data API key, the activities, survey structures, responses, users, workspaces and mailing lists a council or government team runs. It is built from Delib's public documentation only: the Data API's OpenAPI document (/api/1/openapi.json, paths and parameters, no schemas), the JSON examples on the Data API "API specification" page, and the field list on the Public API "Version 2.4 reference" page.

Once it's connected, an engagement officer can ask things like:

  • "Which of our consultations are open right now, and when do they close?"

  • "What questions does the Kings Gardens survey ask, and on which pages?"

  • "How many responses has it had, how many are complete, and what are people saying about the old oak?"

  • "Who on our site is in the Parks workspace?"

  • "How many people are on the mailing list for the library consultation?"

Tools

Tool

What it does

API calls

search_public_activities

Published, public activities with every documented search argument passed through under its documented name: free text (tx), postcode (pc), state (st), audience (au), interest (in), department (de), area (ar), a date range on the open or close date (dk, fd, td), activity type (ct) and the fields tier (basic, extended, all). No credentials needed.

GET /api/2.4/json_search_results

get_public_activity

One published activity by department id and activity id (the two slugs in its URL). No credentials needed.

GET /api/2.4/json_consultation_details

whoami

The name of the configured API key as the site sees it; a key the site does not recognise (the documented _anonymous_ answer) is reported as an error that says what to check.

GET /api/1/whoami

list_activities

Every activity the key can see: uid, title, state, start and end date, path. The endpoint documents no parameters, so the state and keyword filters are applied locally.

GET /api/1/activities

get_activity

One activity by uid. Delib documents no response shape for this endpoint, so the record is passed through as sent, with contact-like fields withheld and text redacted.

GET /api/1/activities/{uid}

get_survey_structure

The survey as a tree: survey settings, each page with its questions, each question with its components (whose ids key the answers in responses), plus any question or component that does not fit under a page.

GET /api/1/activities/{uid}/survey, /pages, /questions, /components

list_responses

Responses in batches with the documented batch_size/batch_start parameters, re-requesting with the batch_start of each next_batch_url (or with batch_start plus the number received) while next_batch_url or total says more exist, and stopping at the documented null: id, completed and deleted flags, answers keyed Label (component_id), with counts of completed and deleted responses. Continue with batch_start.

GET /api/1/activities/{uid}/responses_batched

get_response_answers

One response with its answers, from the documented single-response body (which includes the answers map).

GET /api/1/activities/{uid}/responses/{id}

list_workspaces

The site's workspaces, or one by uid. Response shape not documented; passed through with redaction.

GET /api/1/workspaces, GET /api/1/workspaces/{uid}

list_users

Registered user accounts, filtered by the documented fullname, email and workspace_uid parameters, passed through as given. Response shape not documented; names are returned, contact fields withheld by default.

GET /api/1/users

get_mailing_list

An activity's mailing list: the number of subscribers and each record's non-contact fields by default, the addresses only on request. Response shape not documented.

GET /api/1/activities/{uid}/mailinglist

The two Public API tools are always registered. The nine Data API tools are registered only when CITIZENSPACE_API_KEY and CITIZENSPACE_API_SECRET are set.

There are no write tools. The OpenAPI document lists POST /api/1/activities/{uid} ("Change the details for an activity") and POST .../responses/{id} ("Mark a response as completed / not completed"), but their request bodies are typed as null and described only as "A mapping of the fields and values to update": no field names, no example. A tool cannot send a body it cannot validate, so update_activity and update_response are left out, as are the documented clear, remove and restore actions, answer updates and deletions, response creation, GET .../responses (the unbatched list), GET .../mailinglist/{subscriber_id}, GET .../answers, GET .../answers/{component_id} (which the spec says may download a file) and GET /api/1/users/{user_id}. CITIZENSPACE_ALLOW_WRITES is not read.

Related MCP server: british-cybersecurity-mcp

Setup

Requires Node 18 or later.

npm install
npm run build

For the public tools you only need your site's subdomain. For the Data API you need an API key: as the "Generating API keys" page documents, a site admin creates one under Site Settings > API (the Data API has to be enabled on the site first; Delib's help site says this is done per customer on request), gives it permissions (the read ones used here are activities.search, activities.read, activities.responses.read, activities.mailinglist.read, users.search, users.read, workspaces.search, workspaces.read), and copies the Key and Secret. The server sends them as HTTP Basic key:secret, as the "Basic Auth headers with Citizen Space" page documents.

Claude Desktop: add this to claude_desktop_config.json:

{
  "mcpServers": {
    "citizenspace": {
      "command": "node",
      "args": ["/absolute/path/to/citizenspace-mcp/dist/index.js"],
      "env": { "CITIZENSPACE_INSTANCE": "your-site", "CITIZENSPACE_API_KEY": "your-key", "CITIZENSPACE_API_SECRET": "your-secret" }
    }
  }
}

Claude Code:

claude mcp add citizenspace -e CITIZENSPACE_INSTANCE=your-site -e CITIZENSPACE_API_KEY=your-key -e CITIZENSPACE_API_SECRET=your-secret -- node /absolute/path/to/citizenspace-mcp/dist/index.js

Variable

Required

Meaning

CITIZENSPACE_INSTANCE

yes, unless CITIZENSPACE_BASE_URL is set

The subdomain of your site: demo for https://demo.citizenspace.com. Letters, digits and hyphens only; anything else stops the server at start-up.

CITIZENSPACE_API_KEY

for the Data API tools

The Key of an API key from Site Settings > API. Must be set together with the secret.

CITIZENSPACE_API_SECRET

for the Data API tools

The Secret of that API key. Never logged; the key, the secret and the base64 credential are replaced by [redacted] in any body the site or a proxy echoes into an error message.

CITIZENSPACE_BASE_URL

no

Overrides the site URL, e.g. https://demo.citizenspace.com. Used by the tests.

Safety defaults

  • Every tool is read-only and carries the MCP readOnlyHint annotation. There is no write tool to enable (see above).

  • Respondents, subscribers, officers and users are third parties. By default:

    • in responses, the standard respondent-management answers (opsuite.respondentmanagement.*: name, organisation), the email answer (quickconsult.email*), the IP address and browser answers (__userinfo_ip, __userinfo_useragent) and any answer whose label asks for a name, organisation, email, phone, address, postcode, date of birth, NHS number, National Insurance number, passport, signature or username are withheld and their keys listed under withheld_answers; the remaining answers are returned with email addresses replaced by [email redacted], a date that follows "born", "DOB", "d.o.b.", "date of birth", "birth date" or "birthday" by [date of birth redacted], phone-number-like sequences by [phone redacted], 13-19 digit numbers that pass the Luhn check by [card number redacted], ten-digit numbers that pass the NHS modulus-11 check by [NHS number redacted], National Insurance numbers by [NI number redacted] and UK postcodes, in either case, by [postcode redacted]. A free-text answer can still contain personal data no pattern catches (a street address such as "4 Plymouth Road", a health condition, a name in a sentence, a date of birth given without one of those words, a passport or driving-licence number), so treat response text as personal data even by default. Demographic questions (age, ethnicity, disability, religion) are not withheld: on a consultation they are usually the equalities-monitoring part of the analysis itself;

    • on public activities, contact_phone and contact_email are withheld; contact_name, contact_jobtitle and contact_team (the officer's published contact block) are returned;

    • on the records whose shape Delib does not document (users, workspaces, mailing-list subscribers, activity detail), any key whose name contains a word that suggests contact or identity data (email or e_mail, phone, telephone, tel, mobile, fax, address, postcode, zip, dob, birth, birthday, birthdate, ip, user agent, nhs, passport, national insurance, ni number, bank, iban, sort code, card, username) is withheld and listed under withheld_fields. The key is normalised first (camelCase split, lower-cased), so contact_email, emailAddress, dateOfBirth, homeAddress, IPAddress, nhsNumber and address1 are all caught, while description, title, hotel and shipping are not; a key that names the data without one of those words (fullname is kept on purpose; identifier, contact on its own) is not. A mailing list served as bare address strings is withheld entirely;

    • every other string (titles, survey and page text, question and component labels, answers, names of people, audiences, areas and interests, URLs, paths, error messages, the excerpt of a non-JSON body) goes through the same redaction. Names are returned, minus any email or phone typed into them.

    • include_contact_details=true on any tool returns everything as stored.

    • The text patterns are heuristics. Phone: international numbers written with + or 00, UK numbers with a bracketed area code, and UK-style 0… numbers of 9 to 11 digits; other digit strings starting with 0 are redacted too, while 32-character uids, response ids such as ANON-XXXX-YYYY-C and timestamps are left alone. Postcode: one or two letters, a digit, an optional letter or digit, an optional space, a digit and two letters, upper or lower case, where the last two letters are never C, I, K, M, O or V (as in real postcodes), so "5pm" and "3cm" are not caught but "B12 5th" is redacted. NI number: any two capitals, six digits and A-D, with or without spaces, not only the prefixes HMRC allocates. NHS and card numbers: only digit strings that pass the documented check digit, which one arbitrary string in eleven (NHS) or ten (card) also passes, so a long numeric id can be redacted by mistake. Date of birth: only when introduced by one of the words above, in dd/mm/yyyy, yyyy-mm-dd or "12 March 1985" form. Nothing catches a name, a street address, or a date of birth given without one of those words.

  • Nothing is ever downloaded: GET .../answers/{component_id} ("Read or download the answer") is not used.

  • IDs are checked before any call is made: activity and workspace uids and response ids must be single path segments of letters, digits, _ and - (up to 64 characters; every documented example is 32 lowercase hex characters, but the spec types them as plain strings); Public API dept and id are slugs of letters, digits, ., _ and -. Public API dates take the documented yyyy/mm/dd (or yyyy-mm-dd, converted) and real calendar dates only, and date_from/date_to are refused without date_kind, because the reference says they "must be used in conjunction with dk".

  • The Public API tools never send the Authorization header (the reference says no authentication is required and the access level is that of a public visitor); the Data API tools send it on every call.

  • Delib documents no rate limit for either API. Requests are spaced 250 ms apart (about four per second). A 429 is retried at most twice, waiting for Retry-After (whole or fractional seconds, or an HTTP-date; 2 s then 4 s when the header is absent or unreadable). Each wait is capped at 10 seconds so a single request stays well under the MCP client's default 60-second request timeout: if the site asks for a longer wait the call gives up at once and the message says how long to wait. The cap is per request, not per tool call: list_responses can make up to 20 requests in one call.

  • 502, 503 and 504 are retried the same way for GET only (every call here is a GET); when all three attempts fail the error says the site may be unavailable and to try again in a few minutes, without the gateway's HTML. On any other status a body that is not JSON is quoted (first 300 characters) only when it is not markup, so a proxy's 404 page is not passed on either.

  • A 200 whose body is not JSON (a proxy or a login page in the way) is reported as an error naming CITIZENSPACE_INSTANCE / CITIZENSPACE_BASE_URL, never as an empty list; a responses_batched body that is JSON but has no responses list (for example the bare list of the unbatched endpoint) is reported as an error naming the shape received, never as an empty list.

  • Any text quoted from a body into an error (the documented error_message, the excerpt of a non-JSON body) is scrubbed of the configured key, secret and base64 credential before it is cut to length, so a site or proxy that echoes the Authorization header cannot put it, or a prefix of it, into a tool result; the rest of the excerpt goes through the redaction above.

  • A rejected key produces a message that names the two variables, where the key comes from, and the permissions the call may be missing; the documented _anonymous_ answer from whoami gets the same treatment. A key without a secret, or a secret without a key, stops the server at start-up.

Tests

npm test

The test suite:

  1. Checks the schemas against the documentation they were written from. Delib's OpenAPI document declares no schemas, so test/schemas.mjs was written by hand from the JSON examples on the Data API "API specification" page (copied into test/documented-examples.mjs, with the snippets that are not valid JSON as printed, the two whoami bodies and list fragments with trailing commas, transcribed into the records they show: every documented key required, no other keys, types as shown). The first check validates each schema against the example it came from (whoami, activities, both documented error shapes, survey, page, two questions, two components, a response, a batched list), confirms the OpenAPI document (downloaded from demo.citizenspace.com/api/1/openapi.json to spec.json on the first run) has 26 operations, no components, every Data API endpoint this server calls, the documented batch_size/batch_start and fullname/email/workspace_uid parameters, and null request-body schemas on the two update operations, and that the mock's key and secret are obviously fake values that appear nowhere in the documentation. The Public API reference lists fields without a JSON example, so its schema is checked against one record observed on the demo site (see Status) and against the fixtures. The second check validates every fixture record against the schemas, including every fields tier of the public records.

  2. Starts a local mock of a site: the Data API under /api/1 with HTTP Basic key:secret, a 401 with the documented error body for anything else, whoami answering the key's name or the documented _anonymous_, 404s for unknown uids and ids, responses_batched with the documented batching object and next_batch_url (capped at 10 per batch so a 24-response activity spans three batches), the documented user filters, and the Public API under /api/2.4 with no authentication, the three fields tiers, the search arguments and a 404 for an unknown dept/id; injected failures on any endpoint (a JSON body, an HTML page, or a page with a given text), and a one-off 429 on the first GET /api/1/activities. The third check validates the mock's responses against the schemas.

  3. Starts the built server and drives it over stdio with the official MCP client: 29 checks (32 in the whole suite) covering every tool, tool annotations, no write tool even with CITIZENSPACE_ALLOW_WRITES=true, the public-only mode without credentials and the three start-up refusals (key without secret, bad instance, no instance), the host derived from CITIZENSPACE_INSTANCE alone and no request at start-up (a server pointed at the mock is up before the mock sees anything), every documented Public API argument passed through under its documented name with yyyy-mm-dd converted, the local refusals (date range without date_kind, month 13, 30 February and 31 April, bad enums, bad slugs), the fields tiers, contact withholding and text redaction by default, including an email in a link's URL and a phone number in an area's name, and their return on request, get_public_activity with its 404, whoami and its _anonymous_ error, list_activities after a 429 retry that waited for Retry-After with its local filters, get_activity's pass-through, the survey tree with orphans, batching across three batches ending at next_batch_url null with max_results and batch_start continuation and a start past the end, batching when next_batch_url is absent but total says more exist, when there is no batching object at all, when total is reached without a next_batch_url, and when the documented null contradicts total (null wins), a bare list or any other body without a responses list reported as an error, response withholding and redaction by default (a signature answer withheld on its label; a date of birth, NHS, NI and card number and a lower-case postcode redacted in one free-text answer, the street address in it not) and as stored on request, one response by id, the user filters passed through exactly with contact fields, camelCase ones included, withheld and listed, workspaces with their withheld fields listed by name, the mailing list with no address in the default output (records and bare strings), bad ids refused before any call, the 404 messages, the 429 retry in the seconds, fractional-seconds and HTTP-date forms and the 2 s fallback without the header, the wrapped error shape, giving up after three attempts on a persistent 429 and at once on a Retry-After above the cap, a 502 retried and a triple 503 reported with advice and without the gateway HTML, a 200 with a non-JSON body reported as an error, an HTML 404 page not quoted, a 400, a 401 and a 200 login page that echo the Authorization header, the key and the secret never putting them (or a prefix of the credential) into a tool result, that every Data API request carried Basic base64(key:secret) and every Public API request no credentials, each to a documented method and path (the Data API templates from the OpenAPI document, the two Public API methods from the reference), and the 401 message for a wrong secret while the public tools keep working. The suite takes about 30 seconds.

Status

This is a working prototype. It has not yet been run against the live Data API, because it was built without a Citizen Space site or API key (there is no self-serve trial; Delib enables the Data API per customer site). The only live requests, all to the documented demo site demo.citizenspace.com and none with a real key, were: during the research that chose this prototype, an unauthenticated GET /api/2.4/json_search_results?st=open (200, two open activities), GET /api/1/whoami both without credentials and with a made-up key (the body was _anonymous_ both times; the status code was not recorded) and an unauthenticated GET /api/1/activities (401 with the documented "Forbidden: Unauthorized: activity_search failed permission check" body); and on 28 September 2026, while building, four unauthenticated Public API requests (json_search_results?st=open, the same with fields=all, json_consultation_details for one of the results, and one with an unknown dept and id, which answered 404 as documented) to check the Public API reference against real output. Everything below should be confirmed on a real site:

  • The Public API's field names. On the demo site the extended tier spelled participation_url as participate_url and resultsdate as resultdate, added activity_type, workspace_id and workspace_title that the reference does not list, and returned null for why and what_happens_next where the reference describes text. The server reads both spellings and tolerates the nulls; the observed record is kept in test/documented-examples.mjs marked as observed, not documented.

  • whoami: the page prints its bodies as { "Test Key" } and { "_anonymous_" }, which is not valid JSON. The server accepts a bare JSON string or an object; the mock answers a bare string. Whether a wrong key gets _anonymous_ with a 200 or a 400 (the page files it under 400; the research probe saw the _anonymous_ body but did not record the status): the server reports both as a credentials problem, and the tests cover the 400 form.

  • The status code for a missing or wrong key on the other endpoints: the page files the "Forbidden: Unauthorized: ... failed permission check" body under a 400 tab; the research probe of /api/1/activities got a 401. The server treats 401, 403, and a 400 whose message says unauthorised or permission check, as a credentials problem (the tests cover 401, 403 and the whoami 400).

  • Which of the two documented error shapes ({"error_message"} or {"error": {"error_message"}}) the site uses; both are read.

  • GET /api/1/activities: whether it pages (nothing is documented; the server reads it as one list), whether it includes private, draft or archived activities, and the full set of state values (only open appears in the examples).

  • responses_batched: the default and maximum batch_size (the examples use 10; the server asks for up to 50), whether next_batch_url is absolute or relative (the server only reads its batch_start, falling back to batch_start plus the number received), whether it is ever absent rather than null (the server then keeps paging while total says more exist, or until an empty batch when there is no batching object) and whether total counts responses that the batches leave out, such as deleted ones (the documented null ends paging even when total says otherwise), whether deleted responses are included with deleted: true or left out, what a batch_start past the end returns (the mock answers an empty batch with next_batch_url null), and the order of responses.

  • The types of answer values. The examples show strings and lists of strings; anything else is passed through the same redaction.

  • The response shapes of GET /api/1/activities/{uid}, /mailinglist, /api/1/users and /api/1/workspaces, which the page does not document at all. The mock serves the list record for the activity detail, {id, fullname, email, workspace_uid, telephone} users (from the documented filter names), {uid, title} workspaces and {subscriber_id, email, name} subscribers; the server never depends on those names, but the default output of those tools is only as safe as the key-name heuristic, so check what a real record contains before relying on the default view.

  • How the fullname and email user filters match (substring, exact, case) and whether GET /api/1/users pages.

  • The permissions each endpoint needs (the "Generating API keys" page lists the names but not which endpoint needs which) and what a key without the right one answers.

  • Rate limits: nothing is documented anywhere in the pages read, so the 250 ms spacing here is a guess on the polite side.

  • The pc (postcode) search argument: the mock accepts and ignores it, so only the pass-through is tested.

Going to production

This version runs locally over stdio, with the site's own API key, and only reads. For councils to connect from claude.ai or ChatGPT without handling keys, the next step is a remote server (Streamable HTTP) behind OAuth, hosted by Delib, a run of the suite against a real site to settle the points above, and then a listing in the Claude and ChatGPT connector directories.

Licence

MIT. Built by Alexandru Dragoș (alexandru.dragos96@gmail.com) with an AI agent (Claude) working under his direction.

Available Tools

2 tools
get_public_activityGet a public activityA
Read-only

Overview of one published activity by department id and activity id (the two slugs in its URL), from the unauthenticated Public API (json_consultation_details). Same fields as search_public_activities.

ParametersJSON Schema
NameRequiredDescriptionDefault
idYesActivity ID, e.g. spring-planting-in-kings-gardens
deptYesDepartment ID, e.g. parks-and-recreation
fieldsNoall
include_contact_detailsNoInclude the activity's contact phone and email and stop redacting text

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint and openWorldHint, so safety is covered. The description adds genuine unannotated context: this hits the unauthenticated Public API and is the json_consultation_details endpoint, and that its field set matches the search tool. It doesn't note whether unpublished activities error or that contact data is normally redacted.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single, dense sentence that front-loads the resource and identity scheme; no filler, no restatement of the title.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, 'Same fields as search_public_activities' usefully signals the return shape, and the URL-slug framing explains how to address the resource. Only the undescribed 'fields' enum parameter remains a gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Explains that dept and id are the two slugs appearing in the activity URL, which is more than the schema's example strings convey, linking the parameters to a real-world source. It says nothing about the 'fields' enum parameter, which the schema leaves undescribed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific resource and scope: retrieves one published activity, addressed by department id and activity id (the two URL slugs), from the unauthenticated Public API. The singular 'one' plus 'Same fields as search_public_activities' cleanly distinguishes it from the plural sibling, though it never states the verb explicitly beyond 'Overview'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Implicitly routes the agent: use this when you have both slugs and want a single activity, versus search_public_activities for discovery. No explicit when-to-use/when-not or prerequisite statement (e.g. what happens if the activity is unpublished).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_public_activitiesSearch public activitiesA
Read-only

Published, public consultations and other activities on the site, from the unauthenticated Public API (json_search_results). Every documented search argument is passed through: free text, postcode, state, audience, interest, department, area, a date range on the open or close date, activity type. fields=basic gives id, title, url, status, overview and dates; extended adds department, type and participation URL; all adds the 'why' and 'what happens next' text, contact details, related links, documents, audiences, areas and interests. Officer contact phone and email are only returned with include_contact_details.

ParametersJSON Schema
NameRequiredDescriptionDefault
textNoFree text search on title and overview, case-insensitive (argument tx)
stateNoopen, forthcoming or closed (argument st)
fieldsNoWhich groups of fields to returnbasic
area_idNoOne of the area IDs configured on the site (argument ar)
date_toNoEnd of the date range, yyyy/mm/dd (argument td)
postcodeNoPostcode, partial allowed, e.g. BS8 (argument pc)
date_fromNoStart of the date range, yyyy/mm/dd (argument fd)
date_kindNoWhich date the range applies to: op (open date) or cl (close date); required with date_from/date_to (argument dk)
audience_idNoOne of the audience IDs configured on the site (argument au)
interest_idNoOne of the interest IDs configured on the site (argument in)
max_resultsNoThe API returns every match in one response; only the first max_results are shown
activity_typeNoQuickConsult (online survey), File (email/postal), Document (offline) or Link (argument ct)
department_idNoThe ID of the department the activity sits within (argument de)
include_contact_detailsNoInclude the activity's contact phone and email (fields=all) and stop redacting emails, phone numbers and postcodes from text

TDQS

A4.1/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the readOnly/openWorld annotations by disclosing the field-tier behavior (basic vs extended vs all and exactly what each adds), the redaction behavior tied to include_contact_details ('stop redacting emails, phone numbers and postcodes from text'), and that officer contact details are gated on that flag.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single dense paragraph that front-loads the purpose before the argument list; every clause carries information, though it reads as a wall of text and would scan better as bullets.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With 14 parameters, no output schema, and no annotations covering returns, the description fills the gap by describing what each argument does and what the three field tiers actually return — enough for an agent to call it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description earns more by explaining the otherwise thin 'fields' enum ('Which groups of fields to return') in concrete terms and clarifying the relationship between include_contact_details, fields=all, and text redaction.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Names a specific verb+resource (search public activities/consultations) and scopes it precisely: 'Published, public consultations and other activities on the site, from the unauthenticated Public API'. An agent can distinguish it from get_public_activity by the plural/search framing, but the description never explicitly contrasts the two siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage context is implied through the enumeration of supported search arguments (free text, postcode, state, etc.) and the note that it uses the unauthenticated Public API, but there is no explicit 'use this when you want to search vs. use get_public_activity when you have an id' guidance or any exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv0.1.0
    • First observedget_public_activity
    • First observedsearch_public_activities

TDQS

A3.9/5.0

Scored across 2 tools

Disambiguation5/5

The two tools are cleanly separated: search_public_activities performs a bulk/query lookup and get_public_activity retrieves a single record by department id and activity id. There is no plausible way to confuse them.

Naming Consistency5/5

Both names follow a strict verb_noun snake_case pattern (search_public_activities, get_public_activity) and the resource noun is identical modulo pluralization, which is exactly what makes a set predictable.

Tool Count3/5

Two tools is defensible for a read-only wrapper over an unauthenticated public API, but it is at the thin end: the descriptions hint at a much larger underlying surface (departments, areas, interests, contact details) that has no dedicated tool.

Completeness3/5

Search plus detail covers the core read lifecycle and the field-level flags are rich, but there is no way to enumerate reference data (departments, areas, interests), no explicit paging control, and no authenticated or submission-oriented operations, leaving agents at a dead end for discovery tasks.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers