From 89912e82697ebc2912990547c1c81cbc889f0e20 Mon Sep 17 00:00:00 2001 From: Graham Barber Date: Fri, 17 Jul 2026 21:39:19 -0700 Subject: [PATCH] archive add-mcp-server change and sync specs --- .../2026-07-17-add-mcp-server}/.openspec.yaml | 0 .../2026-07-17-add-mcp-server}/README.md | 0 .../2026-07-17-add-mcp-server}/design.md | 0 .../2026-07-17-add-mcp-server}/proposal.md | 0 .../specs/auth/spec.md | 0 .../specs/categorization/spec.md | 0 .../specs/mcp-server/spec.md | 4 +- .../2026-07-17-add-mcp-server}/tasks.md | 0 openspec/specs/auth/spec.md | 32 ++++- openspec/specs/categorization/spec.md | 42 ++++-- openspec/specs/mcp-server/spec.md | 134 ++++++++++++++++++ 11 files changed, 193 insertions(+), 19 deletions(-) rename openspec/changes/{add-mcp-server => archive/2026-07-17-add-mcp-server}/.openspec.yaml (100%) rename openspec/changes/{add-mcp-server => archive/2026-07-17-add-mcp-server}/README.md (100%) rename openspec/changes/{add-mcp-server => archive/2026-07-17-add-mcp-server}/design.md (100%) rename openspec/changes/{add-mcp-server => archive/2026-07-17-add-mcp-server}/proposal.md (100%) rename openspec/changes/{add-mcp-server => archive/2026-07-17-add-mcp-server}/specs/auth/spec.md (100%) rename openspec/changes/{add-mcp-server => archive/2026-07-17-add-mcp-server}/specs/categorization/spec.md (100%) rename openspec/changes/{add-mcp-server => archive/2026-07-17-add-mcp-server}/specs/mcp-server/spec.md (97%) rename openspec/changes/{add-mcp-server => archive/2026-07-17-add-mcp-server}/tasks.md (100%) create mode 100644 openspec/specs/mcp-server/spec.md diff --git a/openspec/changes/add-mcp-server/.openspec.yaml b/openspec/changes/archive/2026-07-17-add-mcp-server/.openspec.yaml similarity index 100% rename from openspec/changes/add-mcp-server/.openspec.yaml rename to openspec/changes/archive/2026-07-17-add-mcp-server/.openspec.yaml diff --git a/openspec/changes/add-mcp-server/README.md b/openspec/changes/archive/2026-07-17-add-mcp-server/README.md similarity index 100% rename from openspec/changes/add-mcp-server/README.md rename to openspec/changes/archive/2026-07-17-add-mcp-server/README.md diff --git a/openspec/changes/add-mcp-server/design.md b/openspec/changes/archive/2026-07-17-add-mcp-server/design.md similarity index 100% rename from openspec/changes/add-mcp-server/design.md rename to openspec/changes/archive/2026-07-17-add-mcp-server/design.md diff --git a/openspec/changes/add-mcp-server/proposal.md b/openspec/changes/archive/2026-07-17-add-mcp-server/proposal.md similarity index 100% rename from openspec/changes/add-mcp-server/proposal.md rename to openspec/changes/archive/2026-07-17-add-mcp-server/proposal.md diff --git a/openspec/changes/add-mcp-server/specs/auth/spec.md b/openspec/changes/archive/2026-07-17-add-mcp-server/specs/auth/spec.md similarity index 100% rename from openspec/changes/add-mcp-server/specs/auth/spec.md rename to openspec/changes/archive/2026-07-17-add-mcp-server/specs/auth/spec.md diff --git a/openspec/changes/add-mcp-server/specs/categorization/spec.md b/openspec/changes/archive/2026-07-17-add-mcp-server/specs/categorization/spec.md similarity index 100% rename from openspec/changes/add-mcp-server/specs/categorization/spec.md rename to openspec/changes/archive/2026-07-17-add-mcp-server/specs/categorization/spec.md diff --git a/openspec/changes/add-mcp-server/specs/mcp-server/spec.md b/openspec/changes/archive/2026-07-17-add-mcp-server/specs/mcp-server/spec.md similarity index 97% rename from openspec/changes/add-mcp-server/specs/mcp-server/spec.md rename to openspec/changes/archive/2026-07-17-add-mcp-server/specs/mcp-server/spec.md index 6f2aeea..fdd31ed 100644 --- a/openspec/changes/add-mcp-server/specs/mcp-server/spec.md +++ b/openspec/changes/archive/2026-07-17-add-mcp-server/specs/mcp-server/spec.md @@ -21,8 +21,8 @@ self-authenticated, requiring no server-held session across requests. - **WHEN** a client presents a valid token and issues an MCP `tools/list` request -- **THEN** the system returns the catalog of tools available to that token's - scope +- **THEN** the system returns the full tool catalog regardless of scope; a + write tool invoked with a `read`-scope token is refused at invocation #### Scenario: Handler is transport-only diff --git a/openspec/changes/add-mcp-server/tasks.md b/openspec/changes/archive/2026-07-17-add-mcp-server/tasks.md similarity index 100% rename from openspec/changes/add-mcp-server/tasks.md rename to openspec/changes/archive/2026-07-17-add-mcp-server/tasks.md diff --git a/openspec/specs/auth/spec.md b/openspec/specs/auth/spec.md index 8ff9711..82290c8 100644 --- a/openspec/specs/auth/spec.md +++ b/openspec/specs/auth/spec.md @@ -79,14 +79,36 @@ The system SHALL persist application sessions and OAuth client state (state store, session store) in SQLite. Sessions SHALL be identified by opaque tokens, delivered to the web frontend via HTTP-only cookie; the token format SHALL NOT assume cookie transport, so future native clients can present the same token as -a bearer credential. Every route except login, the OAuth callback, client -metadata, and JWKS SHALL require a valid session. Users SHALL be able to log -out, which destroys the application session. +a bearer credential. The system SHALL additionally accept a bearer API token: a +request that presents no session cookie but carries a valid +`Authorization: +Bearer` API token SHALL be authenticated as that token's +principal, carrying the token's scope and, where present, the creating user's +DID. Bearer resolution SHALL occur in the same request-handling chokepoint as +cookie resolution, and SHALL NOT alter cookie-session behavior. Every route +except login, the OAuth callback, client metadata, and JWKS SHALL require either +a valid session or a valid API token. Users SHALL be able to log out, which +destroys the application session. #### Scenario: Unauthenticated access to a protected route -- **WHEN** a request without a valid session cookie targets any protected route -- **THEN** the system redirects to the login page +- **WHEN** a request without a valid session cookie and without a valid API + token targets any protected route +- **THEN** the system redirects to the login page or, for an API endpoint, + rejects the request as unauthorized + +#### Scenario: Bearer token authenticates an API request + +- **WHEN** a request carries no session cookie but presents a valid + `Authorization: Bearer` API token +- **THEN** the system authenticates it as that token's principal, exposing the + token's scope for downstream authorization + +#### Scenario: Cookie session unchanged + +- **WHEN** a browser request presents a valid session cookie +- **THEN** it is authenticated exactly as before, regardless of any bearer + header #### Scenario: Logout diff --git a/openspec/specs/categorization/spec.md b/openspec/specs/categorization/spec.md index 3b1fa99..5ec1e65 100644 --- a/openspec/specs/categorization/spec.md +++ b/openspec/specs/categorization/spec.md @@ -29,11 +29,14 @@ grouping purely additive. ### Requirement: Append-only categorization event log The system SHALL record every category assignment as an immutable event: -transaction id, category id, source (`rule`, `manual`, or `reconciliation`), the -rule id for rule events, the acting user's DID for manual events, and a -timestamp. A transaction's current category SHALL be the latest event, -denormalized onto the transaction row in the same database transaction as the -event insert. Events SHALL never be updated or deleted. +transaction id, category id, source (`rule`, `manual`, `reconciliation`, or +`agent`), the rule id for rule events, the acting user's DID for manual events, +the acting API token for agent events, and a timestamp. A transaction's current +category SHALL be the latest event, denormalized onto the transaction row in the +same database transaction as the event insert. Events SHALL never be updated or +deleted. An `agent` event SHALL carry the id of the API token that produced it +and SHALL NOT carry a user DID, so an automated actor is never recorded as a +person. #### Scenario: Manual categorization records actor @@ -41,11 +44,18 @@ event insert. Events SHALL never be updated or deleted. - **THEN** a `manual` event is appended with that user's DID and the transaction's cached category is updated atomically with it +#### Scenario: Agent categorization records the token + +- **WHEN** an agent assigns a category to a transaction through the MCP write + tool +- **THEN** an `agent` event is appended recording the acting API token, with no + user DID, and the transaction's cached category is updated atomically with it + #### Scenario: Provenance is visible - **WHEN** a user views a categorized transaction's history -- **THEN** the UI shows every event in order: what assigned it (rule pattern or - person), to which category, and when +- **THEN** the UI shows every event in order: what assigned it (rule pattern, + person, or agent token), to which category, and when #### Scenario: Recategorization preserves history @@ -102,11 +112,13 @@ rule's id. ### Requirement: Manual decisions outrank rules -The system SHALL never allow a rule to overwrite a categorization whose latest -event is `manual`. Rules fire only on transactions with no current category. -When a user creates a rule, the system SHALL offer to retroactively apply it to -existing matching transactions that are currently uncategorized, and SHALL NOT -touch categorized ones. +The system SHALL never allow a rule or an agent to overwrite a categorization +whose latest event is `manual`. Rules fire only on transactions with no current +category. An agent categorization SHALL be permitted where no category is set or +where the latest event is `rule`, `reconciliation`, or `agent`, but SHALL NOT +overwrite a `manual` latest event. When a user creates a rule, the system SHALL +offer to retroactively apply it to existing matching transactions that are +currently uncategorized, and SHALL NOT touch categorized ones. #### Scenario: Rule does not overwrite manual choice @@ -114,6 +126,12 @@ touch categorized ones. transaction's latest event is `manual` - **THEN** the transaction's category is unchanged +#### Scenario: Agent does not overwrite manual choice + +- **WHEN** an agent attempts to categorize a transaction whose latest event is + `manual` +- **THEN** the transaction's category is unchanged and the attempt is refused + #### Scenario: Retroactive application on rule creation - **WHEN** a user creates a rule and accepts the retroactive-apply offer diff --git a/openspec/specs/mcp-server/spec.md b/openspec/specs/mcp-server/spec.md new file mode 100644 index 0000000..33346f0 --- /dev/null +++ b/openspec/specs/mcp-server/spec.md @@ -0,0 +1,134 @@ +# mcp-server Specification + +## Purpose + +Expose Quantum's service layer to AI agents over the Model Context Protocol, so +an agent can take over the repetitive half of the ledger — categorizing +transactions, drafting and probing rules, pulling reports — through a single +transport-only handler that serves both the hosted server and the desktop's +local mount. Agents authenticate with scoped, revocable bearer API tokens +minted in Settings, and every agent write is recorded as first-class `agent` +provenance, never as a person. + +## Requirements + +### Requirement: MCP endpoint over authenticated Streamable HTTP + +The system SHALL expose a Model Context Protocol server at `POST /mcp` using the +Streamable HTTP transport. The endpoint SHALL require a valid bearer API token +and SHALL reject any request without one. The MCP handler SHALL be a +transport-bound request handler that reads neither cookies nor server-side view +state, so the identical handler can serve both a hosted server and an in-process +local mount. The transport SHALL operate statelessly, each request +self-authenticated, requiring no server-held session across requests. + +#### Scenario: Unauthenticated MCP request rejected + +- **WHEN** a request reaches `/mcp` without a valid `Authorization: Bearer` + token +- **THEN** the system does not process any MCP method and responds as + unauthorized, creating no session + +#### Scenario: Authenticated tools listing + +- **WHEN** a client presents a valid token and issues an MCP `tools/list` + request +- **THEN** the system returns the full tool catalog regardless of scope; a + write tool invoked with a `read`-scope token is refused at invocation + +#### Scenario: Handler is transport-only + +- **WHEN** the MCP handler processes a request +- **THEN** it derives the caller's identity solely from the bearer token, reads + no session cookie, and depends on no web-view state, so the same handler can + be mounted outside the web router + +### Requirement: Bearer API tokens + +The system SHALL allow an authenticated user to mint API tokens from Settings, +each with a human-readable label and a scope of `read` or `readwrite`. On a +multi-user server the token SHALL record the DID of the user who created it. The +full token value SHALL be shown exactly once, at creation, and the system SHALL +persist only a hash of it, never the token itself. Each token SHALL be +individually revocable, and the system SHALL record when each token was last +used. A revoked token SHALL be rejected immediately on its next use. + +#### Scenario: Token shown once and stored hashed + +- **WHEN** a user mints an API token +- **THEN** the full token value is displayed a single time, only its hash is + persisted, and the value cannot be retrieved again afterward + +#### Scenario: Last-used recorded + +- **WHEN** a token authenticates an MCP request +- **THEN** the system updates that token's last-used timestamp + +#### Scenario: Revocation takes effect immediately + +- **WHEN** a user revokes a token and a client then presents it +- **THEN** the request is rejected as unauthorized + +#### Scenario: Token labelled for recognition + +- **WHEN** a user views their API tokens in Settings +- **THEN** each token is listed by its label, scope, creation time, and + last-used time, and the token value is not shown + +### Requirement: Read tool catalog + +The system SHALL expose read-only tools, available to any valid token regardless +of scope, that surface the existing service layer without modifying data: +listing accounts with balances and staleness, listing and filtering +transactions, retrieving a transaction's full categorization history, producing +a monthly income-versus-expense report, retrieving the net-worth series, listing +categories, listing rules with their match health, probing a prospective or +existing rule without applying it, and reporting sync status and connection +errors. A read tool SHALL NOT modify any transaction, event, rule, category, +account, or connection. + +#### Scenario: Answering where the money went + +- **WHEN** a client calls the monthly-report tool for a given month +- **THEN** the system returns income and expense totals by category for that + month, computed by the same service the web report uses, and changes nothing + +#### Scenario: Probe does not mutate + +- **WHEN** a client calls the rule-probe tool with a candidate pattern +- **THEN** the system returns the transactions the pattern would match and their + current categorization status, and no transaction or event is modified + +#### Scenario: Read scope suffices for read tools + +- **WHEN** a client authenticating with a `read`-scope token calls any read tool +- **THEN** the tool executes normally + +### Requirement: Write tool catalog gated by scope + +The system SHALL expose write tools — categorizing a transaction, creating a +category, creating a rule, activating or deactivating a rule, applying rules to +uncategorized transactions, and triggering a sync — and SHALL permit them only +to a token whose scope is `readwrite`. A write tool invoked with a `read`-scope +token SHALL be refused without effect. The write catalog SHALL NOT include +creating accounts, claiming SimpleFIN setup tokens, importing CSV files, or +hard-deleting any record. + +#### Scenario: Read token refused a write tool + +- **WHEN** a client authenticating with a `read`-scope token invokes a write + tool +- **THEN** the system refuses the call and makes no change + +#### Scenario: Readwrite token categorizes + +- **WHEN** a client with a `readwrite` token invokes the categorize tool for a + transaction and category +- **THEN** the transaction is categorized via an `agent` event attributed to the + authenticating token + +#### Scenario: Excluded operations absent + +- **WHEN** a client lists available tools +- **THEN** no tool creates an account, claims a bank setup token, imports a CSV, + or hard-deletes a record -- 2.51.2