Skip to content

Mark tools hidden by MCP_PERMISSIONS and sharpen overlapping tool descriptions - #43

Merged
futureweb merged 1 commit into
mainfrom
feat/catalogue-permission-marks
Oct 19, 2026
Merged

futureweb merged 1 commit into
mainfrom
feat/catalogue-permission-marks

Conversation

@futureweb

Copy link
Copy Markdown
Owner

Glama's tool-definition quality score (TDQS 3.9/5) found two problems, and this PR fixes both.

1. References to tools that the permission classes hide

With the default MCP_PERMISSIONS=read, the descriptions, server instructions, prompts and guides named write tools such as add_or_update_event, add_events_bulk, create_library_workout and update_sport_settings. Those tools are missing from tools/list, so Glama called the server "a dead-end for mutations".

This extends the "(full tool set)" mechanism from #28 into mcp_instance.mark_unavailable_tools(text, toolset, permissions), which replaces mark_outside_toolset:

  • A tool of a permission class that is not enabled gets "(needs MCP_PERMISSIONS write)", "(needs MCP_PERMISSIONS destructive)" or "(needs MCP_PERMISSIONS admin)".
  • A tool outside MCP_TOOLSET=core still gets "(full tool set)". When both apply, the reasons share one mark: "(full tool set, needs MCP_PERMISSIONS write)".
  • The marks are applied in tool descriptions and parameter descriptions (tools/list), the server instructions (IntervalsFastMCP.mark_instructions(), called at the end of server.py once every tool is defined), prompts, guide resources and get_guide.
  • A mark goes after the call arguments and after a closing backtick (`add_or_update_event` (needs …), get_durability(...) (full tool set)), so a Markdown code span keeps the exact tool name. Marking an already marked text again changes nothing.
  • Parameter descriptions were not marked before. In the core set, for example, get_activities pointed to get_gear_list without a mark.
  • With read,write, no write tool is marked. Only the admin tools update_sport_settings and add_events_bulk keep "(needs MCP_PERMISSIONS admin)".
  • The write rule in the server instructions was rephrased so that the mark does not end up inside nested parentheses.

Examples (real tools/list / initialize over stdio):

Where Before (MCP_PERMISSIONS=read) After (MCP_PERMISSIONS=read)
validate_workout Use before every workout write (add_or_update_event, add_events_bulk, create_library_workout) to check … Use before every workout write (add_or_update_event (needs MCP_PERMISSIONS write), add_events_bulk (needs MCP_PERMISSIONS admin), create_library_workout (needs MCP_PERMISSIONS write)) to check …
get_sport_settings … change thresholds: update_sport_settings. … change thresholds: update_sport_settings (needs MCP_PERMISSIONS admin).
Instructions Preview first (validate_workout or preview_workout before add_or_update_event; dry_run=true returns the exact request) and show what will change; report the read-back. Preview first and show what will change: validate_workout or preview_workout before add_or_update_event (needs MCP_PERMISSIONS write); dry_run=true returns the exact request. Report the read-back.
Core + read, validate_workout (add_or_update_event, add_events_bulk (full tool set), create_library_workout (full tool set)) (add_or_update_event (needs MCP_PERMISSIONS write), add_events_bulk (full tool set, needs MCP_PERMISSIONS admin), create_library_workout (full tool set, needs MCP_PERMISSIONS write))
read,write, validate_workout (add_or_update_event, add_events_bulk, create_library_workout) (add_or_update_event, add_events_bulk (needs MCP_PERMISSIONS admin), create_library_workout)

Known limitation (possible follow-up): with OAuth, a connection can be granted fewer classes than the server enables. The tools hidden only for that token are not marked, because the marks follow MCP_PERMISSIONS. tools/call still refuses them with the reconnect hint.

2. Overlapping tools

The first paragraph of each description now says when to use the tool and which sibling to use for the other cases. Every description stays at 900 characters or fewer. There are no behaviour changes, renames or merges.

Tool Before (first sentence) After (first paragraph)
get_training_summary Use for training totals of a period grouped by ISO week, month, sport (activity type), gear or in total, including custom activity fields (read-only). Use for training totals of a period computed from the activities, grouped by ISO week, month, sport, gear or in total (read-only). For a quick per-week count in one call use get_weekly_summary; for a review with recovery and plan get_coach_context; for ACWR and monotony get_training_load.
get_weekly_summary Use for a quick per-week overview from Intervals.icu's athlete summary (read-only, one API call). Use for a quick week-by-week overview in one API call: Intervals.icu's own athlete summary per ISO week (read-only). For totals per month, sport or gear, power zones, separate power/HR/pace loads or custom fields use get_training_summary; for a training review with recovery and plan get_coach_context.
get_coach_context Use first for a weekly or general training review: … (read-only, about 2-3k characters). … For period totals use get_training_summary; for load detail get_training_load; for future fitness get_load_projection; for one day's recovery get_recovery_snapshot.
get_training_load Use for acute and chronic training load in detail: … Use for past and current training load in detail: … For the overview use get_coach_context; for planned load get_load_projection; for time and distance totals get_training_summary.
get_load_projection Use for future fitness: … … For past and current load use get_training_load.
get_wellness_data Use for the raw daily wellness records of a date range (default the last 30 days; read-only). Use for the raw daily wellness records of a date range as stored (default the last 30 days; read-only). For one day in context with baselines, load and plan use get_recovery_snapshot; for trends vs the athlete's baseline get_wellness_trends; for food intake and calorie balance get_nutrition_summary.
get_recovery_snapshot Use for the recovery and readiness context of one day in one call (…). … For multi-day trends use get_wellness_trends; for raw records of a range get_wellness_data.
get_wellness_trends Use for multi-day wellness trends against the athlete's own baseline (…). … For one day in context use get_recovery_snapshot; for raw daily records get_wellness_data; for food and calorie balance get_nutrition_summary.
get_nutrition_summary Use for food intake, calorie balance and weight trend over a period (…). … For other wellness metrics use get_wellness_trends (trends vs baseline) or get_recovery_snapshot (one day).

The "X: tool" cross-references at the end of these descriptions moved into the first paragraph. The usage guide's totals line now says the same.

Token counts

These come from a real stdio round trip (futureweb-intervals-mcp, initialize + tools/list, placeholder credentials, no Intervals.icu request). They are tiktoken cl100k counts of the compact tools/list JSON. The catalogue budget in tests/test_catalogue.py uses chars/4.

MCP_PERMISSIONS / tool set tools cl100k before → after chars/4 before → after budget (chars/4)
read / full 52 19,278 → 19,435 (+157) 21,018 → 21,206 23,000
read,write / full 59 23,196 → 23,309 (+113) 25,004 → 25,139 26,500
read / core 20 7,402 → 7,505 8,100 → 8,220 9,000
read,write / core 23 9,562 → 9,634 10,240 → 10,324 11,000
instructions (read) 313 → 318 (1,482 → 1,511 chars)
instructions (read,write) 313 → 312 (1,482 → 1,481 chars)

Tests

  • test_unregistered_tools_are_marked_everywhere_in_a_fresh_process[read, read,write]: runs a fresh process with that MCP_PERMISSIONS and opens a real MCP session (initialize, tools/list, every prompt, every guide resource, get_guide for every topic). It checks that every mention of a package tool carries exactly the expected mark: none for an exposed tool, the reasons for a hidden one. It also checks that read,write has no write mark and that no request leaves the machine.
  • test_unavailable_tool_marks_placement_and_combination: mark placement (backticks, call arguments), combined marks, idempotence, and no false matches (get_events_x).
  • test_core_toolset_marks_tools_outside_the_set: now also covers parameter descriptions.
  • test_overlapping_tools_name_the_sibling_for_the_other_cases: checks the "For … use get_…" sentence and the siblings in the first paragraph.
  • The catalogue token budgets are unchanged and still met.

Checks: ruff check ., mypy src tests and pytest (whole suite) passed. The pylint gate (pylint --disable=C0301 --fail-under=0 --fail-on=E,F) exits 0 and reports no messages in the changed files.

🤖 Generated with Claude Code

…criptions

- Tools whose permission class is not enabled are marked wherever they are named
  ("(needs MCP_PERMISSIONS write|destructive|admin)"): tool and parameter descriptions,
  server instructions, prompts, guide resources and get_guide. Extends the "(full tool set)"
  mark of MCP_TOOLSET=core; both reasons combine in one mark. Marks follow call arguments
  and a closing backtick.
- Overlapping tools (get_training_summary / get_weekly_summary / get_coach_context /
  get_training_load / get_load_projection; get_wellness_data / get_recovery_snapshot /
  get_wellness_trends / get_nutrition_summary) name the sibling to use for the other cases
  in their first paragraph. No behaviour changes.
- Tests: no unmarked reference to an unregistered tool at MCP_PERMISSIONS=read, no write
  marks at read,write (real MCP session in a fresh process), mark placement, siblings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@futureweb
futureweb merged commit bf98d22 into main Oct 19, 2026
10 checks passed
@futureweb
futureweb deleted the feat/catalogue-permission-marks branch October 19, 2026 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant