Skip to content

Project system.* through the correct MCP primitive (resources for read, tools for write) #8

Description

@tercel

Implementation for this repository. Design, evidence and cross-language acceptance live in the tracking issue: aiperceivable/apcore-mcp#15

Why

With sys_modules.enabled = true, all nine system.* modules currently appear in tools/list, including the three write modules. The chain was verified in the TypeScript stack and the mechanism is identical here — system modules register through register_sys_modulesregistry.register_internal(), which sets no discoverability metadata, so the registry listing used to build tools includes them.

First task: confirm the same chain in this repo and record the file/line references in a comment on this issue, so the fix is anchored to verified facts rather than to the TypeScript analysis. Reference points in the TypeScript stack: apcore-typescript/src/sys-modules/registration.ts:386, registry/registry.ts:1096-1112 and :1181-1196, apcore-mcp-typescript/src/index.ts:659/:944.

Target behaviour

Classify by module_id prefix only — PROTOCOL_SPEC §6.6.2 requires that adapters do not invent their own classification mechanism (no reserved tags, no environment variables, no adapter-level switches).

Module Now Target
system.health.summary tool resource apcore://system.health.summary
system.manifest.full tool resource apcore://system.manifest.full
system.usage.summary tool resource apcore://system.usage.summary{?period}
system.health.module tool resource template apcore://system.health.module/{module_id}
system.manifest.module tool resource template apcore://system.manifest.module/{module_id}
system.usage.module tool resource template apcore://system.usage.module/{module_id}{?period}
system.control.update_config tool unchanged — tool + approval
system.control.reload_module tool unchanged — tool + approval
system.control.toggle_feature tool unchanged — tool + approval

Read modules become resources for two reasons: PROTOCOL_SPEC §6.6.2 classifies them as read-only with no side effects, and tools enter the agent's tool-selection space — six management tools an agent should never choose between measurably degrade selection quality.

Change points

File Change
src/apcore_mcp/server/system_surface.py new — prefix constants and classification helpers
src/apcore_mcp/server/factory.py tool building filter out read-only management modules; leave system.control.* alone
src/apcore_mcp/server/factory.py resource handlers take the executor; merge management resources into the existing documentation-resource handler; dispatch reads by scheme
src/apcore_mcp/server/factory.py new resource-templates handler with the three templates
src/apcore_mcp/apcore_mcp.py serve path pass the executor into resource handler registration
src/apcore_mcp/acl_builder.py the ACL template fix (see below)
SYSTEM_PREFIX = "system."
SYSTEM_WRITE_PREFIX = "system.control."
SYSTEM_RESOURCE_SCHEME = "apcore://"


def is_system_read_module(module_id: str) -> bool:
    """Read-only management module: system.* that is not system.control.*"""
    return module_id.startswith(SYSTEM_PREFIX) and not module_id.startswith(SYSTEM_WRITE_PREFIX)

Do not do these three things

This section exists because each of these is easy to reach for and each is wrong.

1. Do not register a second resources/list handler.
setRequestHandler is overwrite semantics. There is already a handler serving docs://{module_id} documentation resources. Registering another one silently removes documentation resources. Management resources MUST be merged into the existing handler, not added alongside it.

2. Do not read management resources without going through the executor.
The existing docs:// handler returns static text, so it needs no executor. Management resources are module invocations: they MUST call the module through the normal execution path so that ACL, approval, audit events and redaction all apply. Reading module state directly from a collector, or calling module.execute() directly, opens a bypass around the governance runtime — in a governance runtime, that is the worst possible defect. ACL_DENIED must surface as an MCP error; do not pre-filter the resource list by permission (PROTOCOL_SPEC §6.6.4 requires backend-driven visibility: the adapter does not maintain a parallel authorization model).

3. Do not model the parameterised modules as static resources.
system.health.module, system.manifest.module and system.usage.module all require module_id. Enumerating one static resource per registered module makes resources/list grow with the application. They MUST be resource templates with RFC 6570 URI templates, registered on the resource-templates handler.

URI ↔ module input mapping

apcore://system.health.summary                        -> {}
apcore://system.manifest.full                         -> {}
apcore://system.usage.summary?period=24h              -> { period: "24h" }
apcore://system.health.module/{module_id}             -> { module_id }
apcore://system.manifest.module/{module_id}           -> { module_id }
apcore://system.usage.module/{module_id}?period=24h   -> { module_id, period }

Parsing rules:

  • scheme MUST be apcore:; anything else is not this handler's URI
  • host segment is the module ID (module IDs are lowercase by EBNF, so case normalisation during URL parsing is harmless)
  • path segments are positional parameters, query parameters are named options
  • an unknown module ID, an unknown query parameter, or a missing required path segment MUST be an error — never silently ignored

Also in this repo (ACL template fix)

  • Replace sys. with system. in this repo's acl_builder doc comments
  • Ship the complete ACL rule template
  • Add the test asserting template targets match registered module IDs

Acceptance

  • With sys_modules.enabled = true, tools/list contains no name starting with system. other than the three system.control.* — regression test, this is the defect that started this issue
  • tools/list still contains the three system.control.* tools, still gated by approval
  • resources/list contains the three parameterless management resources and the pre-existing docs:// resources — both present in one response
  • resources/templates/list contains the three URI templates
  • Reading a management resource that ACL denies returns an MCP error and produces an ACL audit record — proves the executor was not bypassed
  • With sys_modules.enabled = false (the default), neither tools/list nor resources/list contains anything system.*
  • Startup warning fires when system modules are enabled with no ACL and no approval handler, naming both gaps and the configuration that closes them

Out of scope

  • New system.identity / audit.query / config.read / events.tail modules
  • notifications/resources/updated push
  • logging/setLevel
  • Any change to the sys_modules.enabled / ACL-discovery / approval-skip defaults

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions