Skip to content

Add the tool and resource conventions with the first read-only tools - #2508

Merged
pournasserian merged 3 commits into
v2from
2504-tool-conventions
Oct 18, 2026
Merged

pournasserian merged 3 commits into
v2from
2504-tool-conventions

Conversation

@pournasserian

Copy link
Copy Markdown
Member

Closes #2504

What

The tool and resource conventions of F-MCP (#2423) that every capability epic builds its tools on (MCP-02, MCP-03, UX-03), with the first read-only tools for content types and change sets.

  • Core (FluentCMS.Core.Tools):
    • [ExtensionPoint] IToolProvider and IResourceProvider, which built-ins and plugins implement alike (EXT-01, EXT-02).
    • AgentTool: a snake_case name, a title, a description written for a model, a JSON Schema for its input, a read-only flag and a handler. ToolCall passes the arguments and the call's own service scope.
    • ToolResult: the value as JSON, or the errors of a Result with an optional hint that names the call that finds a valid value.
    • ToolArgumentsReader: reports a missing, malformed or unknown argument by name, as FieldSettingsReader does for settings. ToolSchemas builds input schemas that describe every argument with an example and refuse others.
    • AgentResource: a fixed URI or a level 1 URI template, matched one path segment per parameter.
  • Mcp: CoreToolsSetup puts every provider's tools and resources on the server on both transports. A name used twice stops the host at startup.
    • Tools carry the read-only annotation.
    • A success returns the JSON as structured content and as text.
    • A failure is a tool error (isError) with {"errors": [{"code", "message"}], "hint"}, never a protocol error.
  • Built-in tools (FluentCMS.Core.Builtin.Tools):
    • list_content_types
    • get_content_type (slug, optional version)
    • get_schema_document (optional slug)
    • get_change_set (id)
    • get_change_set_diff (id)
  • Built-in resources: fluentcms://schema and fluentcms://schema/{slug}, the schema documents from Add the schema document with field descriptions and examples #2473.
  • CONTRIBUTING.md: the conventions under "Extension points".

Why

MCP-02 asks for descriptions, input schemas and errors written for an LLM, and MCP-03 for resources. F-EXT makes tool providers an extension point that lands with its first consumer. These read-only tools are that consumer, and the write tools follow on the same conventions.

Choices worth a look:

  • Core owns the tool model, not the SDK. Core references no MCP package (ADR-0004). Mcp adapts each AgentTool to an SDK McpServerTool and each AgentResource to an McpServerResource, so a plugin's tool needs nothing from the transport.
  • Arguments first: every tool reads its arguments before any service. A conformance test calls each tool with a service provider that throws, so a tool that skips the check fails. An unknown argument is an error rather than ignored, which catches an agent's typo.
  • Errors carry a hint where one call finds the valid value. An unknown slug points to list_content_types, and an unknown version says to leave it out.
  • Text and structured content carry the same JSON. Clients that read only text get everything. The text keeps quotes and apostrophes unescaped, because a model reads it, not a browser.
  • ValidateOnStart builds the server options when the host starts, so a duplicate tool name fails there, not at the first call.
  • The explicit tools capability is gone: the tool collection now always exists, so the SDK advertises it.
  • Resources fail as protocol errors (ResourceNotFound), because a resource read has no error result. The message is the service's.
  • Reads are not scoped yet: an agent with a key reads every type and set. Type, field and operation scopes belong to the credentials lane of F-MCP: MCP server, agent credentials, scopes and caps #2423.
  • Not in this PR: write tools and session grouping (next slice), bulk operations and the MCP-01 coverage test, entry and entity tools (their lanes, on these conventions), and rule hints (F-RUL).

Tests

Unit:

  • ToolProviderConformanceTests, derived for SchemaTools and ChangeSetTools:
    • unique snake_case names
    • a title and a description for every tool and resource
    • input schemas that are objects refusing other arguments, with a description on every argument and examples that pass an independent validator (NJsonSchema)
    • an unknown argument returns argument_unknown before any service is read
    • every resource URI has a scheme
  • ToolArgumentsReader: values read, then missing, malformed and unknown arguments by name in order; no arguments, null, and a value that is not an object.
  • ToolResult JSON with errors and hint. URI template matching and unescaping. Tool name rules.

Integration:

  • Over HTTP on SQLite and PostgreSQL:
    • tools/list returns the five tools with descriptions, object schemas and the read-only annotation.
    • list_content_types and get_content_type return the current version and version 1.
    • get_schema_document returns the same JSON as ISchemaDocuments, for the site and for one type.
    • get_change_set and get_change_set_diff on a stored set show its state, initiator and operation, and the added field with its after value.
  • An unknown slug, version and change set each return their service code, with a hint where there is one. A malformed and an unknown argument are reported together.
  • Resources list the site document and the type template. Reading returns application/schema+json, and an unknown slug is refused with the service's message.
  • Over stdio in-process on both providers, the tools answer the same way.
  • A second provider with a built-in tool's name stops the web host at startup.
  • The Add the MCP server host over streamable HTTP and stdio #2481 and Authenticate agents with their keys on both MCP transports #2487 tests now expect the built-in tools in the list.

The full suite ran locally against a postgres:17 container as well as SQLite, and markdownlint passes. No new package and no new project reference.

Checklist

  • Linked to an issue a maintainer has confirmed
  • Targets v2
  • Tests or docs updated where behaviour changed
  • No tool-generated co-author trailers or footers

@pournasserian pournasserian added this to the M1 Governance core milestone Oct 18, 2026
@pournasserian pournasserian added type:feature New capability v2 FluentCMS v2 (v2 branch) area:mcp MCP server and agent credentials (MCP) labels Oct 18, 2026
@pournasserian
pournasserian merged commit 87200ef into v2 Oct 18, 2026
2 checks passed
@pournasserian
pournasserian deleted the 2504-tool-conventions branch October 18, 2026 23:13
pournasserian added a commit that referenced this pull request Oct 18, 2026
Closes #2509

## What

The write tools of F-MCP (#2423) for content types and change sets, on
the conventions from #2508. An agent proposes schema changes and submits
them, and a person reviews and publishes them.

- **Change sets**: `open_change_set` (rationale, sources),
`submit_change_set` (open to checks running) and `reopen_change_set`
(checks failed to open), for the agent's own sets only (#2503).
- **Content types**: `create_content_type` (name, slug, definition) and
`revise_content_type` (slug, base version, definition).
- Each stores the new version, unpublished (#2482), and adds the
matching schema operation to a change set in one unit of work, so either
both are written or neither is.
- The set is a `change_set_id` from `open_change_set`, or a `session_id`
with a rationale and sources, which opens the session's set on its first
call (CHG-02).
- Each write result carries the content type, the change set and a
`next` sentence that says what comes next.
- **Core**:
- `IChangeSetDirectory.GetSessionSetAsync` reads the caller's open set
for a session id.
- `IAgentPermissions.CheckChangeSetSizeAsync` takes the number of
operations a write is about to add.
- `ToolArgumentsReader` reads lists of strings, objects, optional ids
and required whole numbers.
- **CONTRIBUTING.md**: the read and write tools and what is live when.

## Why

MCP-01 asks for tools for every capability, and the governance model
needs everything an agent writes to wait in a change set for a person.
With #2482, a stored content type version is not published until its set
is. With this PR, every version an agent stores is also in one of its
change sets, and no tool approves, rejects or publishes (MCP-06).

Choices worth a look:

- **One unit of work for both writes.** Each directory finds its
expected failures before its own unit of work, so the tool's outer one
undoes the version when the change set refuses the operation, for
example because of the item's version, a cap or ownership.
- **Caps are checked before the write too.** A refused cap inside the
outer unit of work would roll back its own security event (SEC-08, the
limit noted in #2503). The tool therefore first reads the target set and
asks `IAgentPermissions` with the operation it is about to add, or asks
for a new set when the session has none. That refusal commits its event,
and nothing is stored. The set checks again on the add, which settles a
race.
- **`GetSessionSetAsync` is a small read on the change set directory**,
the one place that knows a session's open set. The pre-check needs it.
- **No operation scope yet**: an agent with any key can propose. Type,
field and operation scopes are the credentials lane's.
- **Create and revise of one type in one set** is refused by the set
(one operation per item), with the set's own message.
- **Submit and reopen are separate tools**, so each description says
exactly when to call it.
- **Not in this PR**: approve, reject, publish and revert (never for
agents), bulk operations with progress and the MCP-01 coverage test
(next slice), and entry and entity writes (their lanes).

## Tests

Integration, over HTTP:

- On SQLite and PostgreSQL, an agent opens a set, creates `category`,
revises a person's `product` from version 1 and submits the set. The set
holds both operations in order, is in checks running and names the
agent, and the tool call log has all four calls, succeeded.
- On SQLite and PostgreSQL, two creates with one `session_id` land in
one set with the first call's rationale.
- On SQLite and PostgreSQL, these store no version, while the agent's
own valid create is stored:
- a definition with an invalid slug, an unknown field type and a bad
field name returns every error at once
- a create past a cap of one item is refused, and
`agent.items_per_change_set_exceeded` stays in the log
- a create into a person's set and a submit of it are refused as not the
agent's own
  - a create without a target names the missing argument
- A revision from a stale base version returns `version_conflict` with a
hint to read the current version, and the type stays at version 2.
- The conformance tests now cover the write tools: names, described
schemas with valid examples, and unknown arguments refused before any
service is read. Unit tests cover the new reader methods.

The full suite ran locally against a `postgres:17` container as well as
SQLite, and markdownlint passes. No new package and no new project
reference.

## Checklist

- [ ] Linked to an issue a maintainer has confirmed
- [x] Targets `v2`
- [x] Tests or docs updated where behaviour changed
- [x] No tool-generated co-author trailers or footers
pournasserian added a commit that referenced this pull request Oct 19, 2026
Closes #2591

## What

The sixth slice of F-DSK (#2422): read-only desk tools for agents
(MCP-01, "DSK (read-only for review decisions)"), on the tool
conventions from #2508. It fills the DSK row of `McpCoverageTests`.

- **`DeskTools`** (`FluentCMS.Core.Builtin.Tools`), three read-only
tools:
  - `get_desk_counts`: how many items wait in each queue.
- `list_desk_queue`: one page of a queue, ordered as the desk orders it.
- The queues are `awaiting_review`, `checks_failed`, `stale_facts`,
`recently_published` and `reverted`. Awaiting me is a person's queue, so
it is not offered.
- `page` and `limit` are optional (1 to 200, 50 by default), and the
result has the total.
- A set comes with its id, state, rationale, initiator, item count, the
set it reverts and its times. A stale fact is shown as
`list_stale_facts` shows it.
- `get_change_set_review`: the state and since when, the rejection
reason, the set it reverts, and every move in order with who made it and
when.
- Each tool reads its arguments before any service. An unknown queue, a
page below 1 and a limit above 200 are `argument_invalid`. An unknown
set is `change_set_not_found` with a hint.
- **Agents see their own sets only.** #2580 made an agent read only the
change sets it opened (`CheckChangeSetReadAsync`).
- `ChangeSetListQuery` gains `OpenedBy`, which `ChangeSetLists` filters
on.
- `DeskQueues` sets it when the caller is an agent, so an agent's queues
and counts hold its own sets. A person's view is unchanged.
- `get_change_set_review` reads through `GetAsync`, so another party's
set is refused with `agent_not_change_set_owner`.
- **`ChangeSetMoves`** (`FluentCMS.Core.ChangeSets`) names the open and
move operations once. `ChangeSetLists`, the set view and the review tool
used to keep their own copies.
- `DeskToolsConformanceTests`, and the DSK rows of `McpCoverageTests` in
place of the missing one.
- CONTRIBUTING names the desk tools in the MCP paragraph.

## Why

- **Scoping in `DeskQueues`, not in each tool.** The queues are defined
once for the desk and the tools. Applying the read rule where the
queries are built keeps the counts and lists consistent, and a later
tool cannot forget it. The rule itself stays in `AgentPermissions`. The
list only follows it, as `GetAsync` does.
- **`get_change_set_review` next to `get_change_set`.** `get_change_set`
returns the set's content. The review status is the decisions: who moved
it, when, and why it was rejected. An agent needs that to follow up on a
rejection.
- **No awaiting me for agents.** An agent never reviews (MCP-06), so the
queue would only repeat awaiting review.
- **Not in this PR**: writing decisions (people only, MCP-06), and
security events for agents (SEC-08 puts them on the desk for people).

## Tests

Unit:
- `DeskQueuesTests`: an agent's lists and counts carry `OpenedBy` as the
agent, and a person's carry none.
- `ToolProviderConformanceTests` for `DeskTools`, and `McpCoverageTests`
with the new rows.

Integration over MCP, each on SQLite and PostgreSQL:
- An agent opens, writes and submits three sets through its tools, which
are moved to awaiting review and checks failed. The counts and the
queues list those sets in the desk's order with paging and totals. A
person's set awaiting review is left out, and nothing is appended to the
audit log.
- The agent reads the review of its rejected set: the reason, its own
moves and the person's rejection, with `stateSince` at the rejection.
Another party's set is refused with `agent_not_change_set_owner`.
- An unknown queue (`awaiting_me`), a limit of 500, a page of 0 and an
unknown set are refused, the last with a hint.

`DeskQueueTests` now reads the queues as a person, since the scenario's
default caller is an agent. `McpAgents.BuiltInTools` lists the three new
tools.

`dotnet build -warnaserror`, the full suite with the browser tests, and
markdownlint pass locally against a `postgres:17` container as well as
SQLite. No new package and no new project reference.

## Checklist

- [x] Linked to an issue a maintainer has confirmed
- [x] Targets `v2`
- [x] Tests or docs updated where behaviour changed
- [x] No tool-generated co-author trailers or footers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:mcp MCP server and agent credentials (MCP) type:feature New capability v2 FluentCMS v2 (v2 branch)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tool and resource conventions: the tool provider extension point, structured results, the schema resource and read-only tools

1 participant