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_modules → registry.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)
Acceptance
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
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 ninesystem.*modules currently appear intools/list, including the three write modules. The chain was verified in the TypeScript stack and the mechanism is identical here — system modules register throughregister_sys_modules→registry.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-1112and:1181-1196,apcore-mcp-typescript/src/index.ts:659/:944.Target behaviour
Classify by
module_idprefix 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).system.health.summaryapcore://system.health.summarysystem.manifest.fullapcore://system.manifest.fullsystem.usage.summaryapcore://system.usage.summary{?period}system.health.moduleapcore://system.health.module/{module_id}system.manifest.moduleapcore://system.manifest.module/{module_id}system.usage.moduleapcore://system.usage.module/{module_id}{?period}system.control.update_configsystem.control.reload_modulesystem.control.toggle_featureRead 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
src/apcore_mcp/server/system_surface.pysrc/apcore_mcp/server/factory.pytool buildingsystem.control.*alonesrc/apcore_mcp/server/factory.pyresource handlerssrc/apcore_mcp/server/factory.pysrc/apcore_mcp/apcore_mcp.pyserve pathsrc/apcore_mcp/acl_builder.pyDo 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/listhandler.setRequestHandleris overwrite semantics. There is already a handler servingdocs://{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 callingmodule.execute()directly, opens a bypass around the governance runtime — in a governance runtime, that is the worst possible defect.ACL_DENIEDmust 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.moduleandsystem.usage.moduleall requiremodule_id. Enumerating one static resource per registered module makesresources/listgrow with the application. They MUST be resource templates with RFC 6570 URI templates, registered on the resource-templates handler.URI ↔ module input mapping
Parsing rules:
apcore:; anything else is not this handler's URIAlso in this repo (ACL template fix)
sys.withsystem.in this repo'sacl_builderdoc commentsAcceptance
sys_modules.enabled = true,tools/listcontains no name starting withsystem.other than the threesystem.control.*— regression test, this is the defect that started this issuetools/liststill contains the threesystem.control.*tools, still gated by approvalresources/listcontains the three parameterless management resources and the pre-existingdocs://resources — both present in one responseresources/templates/listcontains the three URI templatessys_modules.enabled = false(the default), neithertools/listnorresources/listcontains anythingsystem.*Out of scope
system.identity/audit.query/config.read/events.tailmodulesnotifications/resources/updatedpushlogging/setLevelsys_modules.enabled/ ACL-discovery / approval-skip defaults