specs/022-mcp-change-tool.md
Rama main · réplica de lectura del repositorio original.
# Spec: MCP Change Tool
Status: Implemented.
## Scope
This spec defines the `dagu_change` MCP tool-call contract: input fields,
preview behavior, apply behavior, successful output, resource links, mutation
effects, and tool-level errors.
This spec does not define the MCP endpoint, authentication, API-key surfaces,
`dagu://` URI grammar, DAG YAML language semantics, `dagu_read`,
`dagu_execute`, prompts, subscriptions, Web UI behavior, storage internals,
workspace selection, or MCP SDK internals.
Related specs:
- [020: MCP Server](020-mcp-server.md) defines the MCP endpoint,
authentication, and `dagu://` resource URI model used by this tool.
- [021: MCP Read Tool](021-mcp-read-tool.md) defines how clients read the DAG
spec resource linked from this tool.
## Goal
`dagu_change` gives MCP clients one safe way to validate a DAG YAML change
before writing it. Preview is the default behavior. Apply is explicit and
writes only after the same validation succeeds.
## Behavior
### Tool Identity
Rules:
- The tool name is `dagu_change`.
- The tool supports DAG YAML upsert only.
- A preview call must not create, update, delete, start, enqueue, retry, or
stop any DAG or DAG run.
- An apply call may create or update one stored DAG only when validation
succeeds.
- Audit, metrics, logging, and transport side effects are allowed.
### Input
Tool input is a JSON object. Fields outside this table fail with
`invalid_tool_input`.
| Field | Type | Required rule | Meaning |
| --- | --- | --- | --- |
| `mode` | string | Optional. Defaults to `preview`. | Change execution mode. Supported values are `preview` and `apply`. |
| `type` | string | Optional. Defaults to `upsert_dag`. | Change type. The only supported value is `upsert_dag`. |
| `name` | string | Required. | Target DAG name. |
| `spec` | string | Required. | DAG YAML document to validate and optionally store. |
Rules:
- Supported fields, when present and not `null`, must be strings.
- `null` is treated as absent.
- `mode`, `type`, and `name` are trimmed of leading and trailing whitespace
before validation.
- `spec` is not trimmed or normalized. It fails only when it is empty or all
whitespace.
- Supported values are case-sensitive.
- A missing or empty `name` fails with `invalid_tool_input`.
- A missing or all-whitespace `spec` fails with `invalid_tool_input`.
- An unsupported `mode` fails with `unsupported_change_mode`.
- An unsupported `type` fails with `unsupported_change_type`.
### Change Type
`upsert_dag` validates a DAG YAML document for the supplied DAG name.
Rules:
- Validation uses Dagu's DAG validation rules.
- A validation failure is not a tool-level error.
- When validation fails, the tool returns a successful MCP tool result with
`valid=false`, `applied=false`, and validation errors in the structured
output.
- This spec defines the MCP tool contract. The DAG YAML fields and validation
rules are owned by the DAG YAML specs.
### Preview Mode
Rules:
- `mode=preview` validates the supplied DAG YAML and returns the validation
result.
- `mode=preview` must not write the DAG even when validation succeeds.
- `mode=preview` must not write the DAG when validation fails.
- `applied` is `false` in preview output.
- `created` and `updated` are omitted in preview output.
### Apply Mode
Rules:
- `mode=apply` validates the supplied DAG YAML before writing.
- If validation fails, the call must not write the DAG and returns
`valid=false` with `applied=false`.
- If validation succeeds and no stored DAG exists for `name`, the call creates
the DAG and returns `created=true`, `updated=false`, and `applied=true`.
- If validation succeeds and a stored DAG exists for `name`, the call updates
that DAG and returns `created=false`, `updated=true`, and `applied=true`.
- A successful apply affects only the DAG identified by `name`.
- A successful apply must not start, enqueue, retry, or stop a DAG run.
### Output
A successful MCP tool result has one text content item and structured output.
Text content is mode and validation dependent:
| Condition | Text |
| --- | --- |
| Validation failed. | `DAG spec is not valid; no changes were applied.` |
| Preview validation succeeded. | `DAG spec is valid. Re-run with mode=apply to write it.` |
| Apply validation succeeded and the DAG was written. | `DAG change applied.` |
Structured output has this top-level envelope:
```json
{
"mode": "preview",
"type": "upsert_dag",
"dagName": "nightly-report",
"valid": true,
"errors": [],
"applied": false,
"dag": {},
"dagUri": "dagu://dags/nightly-report/spec",
"references": [
"dagu://reference/authoring",
"dagu://reference/tools",
"dagu://reference/notifications"
]
}
```
Required fields:
| Field | Required shape |
| --- | --- |
| `mode` | Effective mode: `preview` or `apply`. |
| `type` | Effective type: `upsert_dag`. |
| `dagName` | Effective DAG name. |
| `valid` | Boolean validation result. |
| `errors` | Array of validation errors. Empty when `valid=true`. |
| `applied` | Boolean. `true` only when the DAG was written. |
| `dagUri` | Canonical `dagu://dags/{name}/spec` URI for the target DAG. |
| `references` | Exactly `dagu://reference/authoring`, `dagu://reference/tools`, and `dagu://reference/notifications`; order is not normative. |
Conditional fields:
| Field | Required when |
| --- | --- |
| `dag` | Validation succeeds and Dagu can return a normalized DAG model. |
| `created` | `applied=true`. |
| `updated` | `applied=true`. |
Implementations may add fields inside the structured output. Conformance tests
must not require absence of additional fields.
The successful result includes a resource-link content item for `dagUri` when
`dagUri` is present.
Resource-link content item rules:
- MCP content type is `resource_link`.
- `uri` is the canonical `dagu://dags/{name}/spec` URI.
- `name` is `dag_spec`.
- `mimeType` is `application/yaml`.
URI path segments in returned URIs follow Spec 020 escaping.
## Errors
Failed tool calls return an MCP tool result with `isError=true`. When the
transport supports structured output, the structured output is the error object.
Text content may include the error message as a fallback.
The error object has this shape:
```json
{
"code": "invalid_tool_input",
"message": "The spec field is required.",
"mode": "preview",
"type": "upsert_dag",
"dagName": "nightly-report",
"field": "spec",
"dagUri": "dagu://dags/nightly-report/spec",
"details": {}
}
```
`code` and `message` are required. Clients must branch on `code`, not parse
`message`.
Optional field rules:
| Field | Required when |
| --- | --- |
| `mode` | A valid mode has been resolved, or an unsupported mode value was supplied. |
| `type` | A valid type has been resolved, or an unsupported type value was supplied. |
| `dagName` | A non-empty DAG name has been resolved. |
| `field` | Exactly one input field caused the failure. For an unknown field, this is the unknown field name. |
| `dagUri` | A non-empty DAG name has been resolved. |
| `details` | More than one field caused the failure, or the server needs to expose structured error details. |
Common conditions map to codes as follows:
| Condition | Code |
| --- | --- |
| Authentication is required and the request is not authenticated. | `unauthenticated` |
| The accepted credential is not authorized for the requested change. | `unauthorized` |
| Tool input is not a JSON object. | `invalid_tool_input` |
| Tool input contains an unknown field. | `invalid_tool_input` |
| A supported field has a non-string, non-null value. | `invalid_tool_input` |
| `name` is absent or empty after trimming. | `invalid_tool_input` |
| `spec` is absent, empty, or all whitespace. | `invalid_tool_input` |
| `mode` is present but is not `preview` or `apply`. | `unsupported_change_mode` |
| `type` is present but is not `upsert_dag`. | `unsupported_change_type` |
| DAG validation fails. | No tool error. Return `valid=false`. |
| The target DAG cannot currently be read or written during apply. | `resource_unavailable` |
| The server fails unexpectedly while handling the change. | `internal_error` |
## Examples
Preview a valid DAG:
```json
{
"mode": "preview",
"type": "upsert_dag",
"name": "nightly-report",
"spec": "steps:\n - name: hello\n run: echo hello\n"
}
```
Expected structured output subset:
```json
{
"mode": "preview",
"type": "upsert_dag",
"dagName": "nightly-report",
"valid": true,
"applied": false,
"dagUri": "dagu://dags/nightly-report/spec"
}
```
Apply a valid DAG:
```json
{
"mode": "apply",
"type": "upsert_dag",
"name": "nightly-report",
"spec": "steps:\n - name: hello\n run: echo hello\n"
}
```
Expected structured output subset:
```json
{
"mode": "apply",
"type": "upsert_dag",
"dagName": "nightly-report",
"valid": true,
"applied": true,
"created": true,
"updated": false,
"dagUri": "dagu://dags/nightly-report/spec"
}
```
Preview an invalid DAG:
```json
{
"name": "broken",
"spec": "steps:\n - name: [\n"
}
```
Expected structured output subset:
```json
{
"mode": "preview",
"type": "upsert_dag",
"dagName": "broken",
"valid": false,
"applied": false
}
```
Unsupported mode:
```json
{
"mode": "write",
"name": "nightly-report",
"spec": "steps:\n - name: hello\n run: echo hello\n"
}
```
Expected error code:
```json
{
"code": "unsupported_change_mode"
}
```