Note (2026-04-24): After this document was written,
legal_entitywas renamed totenantand the oldtenantwas renamed toorganization. Read references to these terms with the pre-rename meaning.
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Implement two MCP tools (orcha-fpna-context and orcha-fpna-save-dm) for persisting and retrieving Data Maps, plus refactor OAuth scopes from flat mcp:* to domain-scoped docs:*, fpna:*, master-data:*.
Architecture: Append-only fpna_data_map DB table stores Data Map versions per legal entity. The context tool returns the DDP protocol doc (classpath resource) and/or the latest Data Map based on status. The save tool inserts new rows. Both tools auto-resolve legal_entity_id for single-LE identities. OAuth scope refactor touches discovery, flow, middleware, route config, and all existing tool registrations.
Tech Stack: Clojure, PostgreSQL (Migratus migrations), HoneySQL, next-jdbc, cheshire (JSON), clojure.java.io (classpath resources)
Design doc: docs/plans/2026-03-04-fpna-data-map-tools-design.md
Files:
src/com/getorcha/link/oauth/discovery.clj:18-23test/com/getorcha/link/mcp_test.clj:149 (token scope in test helper)Step 1: Update supported scopes in discovery.clj
Replace supported-scopes at line 18-23:
(def ^:const supported-scopes
"Scopes supported by the Link MCP server.
- docs:read - Read access to documents and line items
- docs:write - Write access (future: document upload, annotations)
- fpna:read - Read access to FP&A data maps and context
- fpna:write - Write access to FP&A data maps
- master-data:read - Read access to master data (GL accounts, cost centers, business partners)"
["docs:read" "docs:write" "fpna:read" "fpna:write" "master-data:read"])
Step 2: Update test helper to use new scopes
In test/com/getorcha/link/mcp_test.clj, the get-test-access-token function at line 149 hardcodes "mcp:read" as the scope. Update to grant all scopes needed for testing:
:scope "docs:read docs:write fpna:read fpna:write master-data:read"
Step 3: Run tests to check nothing breaks yet
Run: clj -X:test:silent :nses '[com.getorcha.link.mcp-test]' 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: Tests will fail because mcp:read scope is still required at route and tool level. That's expected — we'll fix those next.
Step 4: Commit
git add src/com/getorcha/link/oauth/discovery.clj test/com/getorcha/link/mcp_test.clj
git commit -m "refactor: update OAuth scopes from mcp:* to domain-scoped"
Files:
src/com/getorcha/link/oauth/flow.clj:47src/com/getorcha/link/oauth/http.clj:154,286src/com/getorcha/link/mcp/http.clj:267-274src/com/getorcha/link/http.clj:78src/com/getorcha/link/mcp/middleware.clj (see below)Step 1: Update default scope in OAuth flow
In src/com/getorcha/link/oauth/flow.clj:47, change:
:scope (or scope "mcp:read")
to:
:scope (or scope "docs:read")
Step 2: Update default scope in OAuth HTTP authorize handler
In src/com/getorcha/link/oauth/http.clj:154, change:
:scope (or scope "mcp:read")
to:
:scope (or scope "docs:read")
Also update the docstring at line ~286 that mentions 'mcp:read' default.
Step 3: Update MCP route — remove transport-level scope gate
The /mcp route currently requires mcp:read scope at the transport level via required-scope. With domain scopes, we need the route to only authenticate (valid token) but leave scope checking to per-tool call-tool.
In src/com/getorcha/link/mcp/http.clj:267-274, change from:
(def route
"MCP transport route with OAuth authentication.
Requires valid OAuth token with mcp:read scope."
["/mcp"
{:post {:no-doc true
:required-scope "mcp:read"
:handler mcp-handler}}])
to:
(def route
"MCP transport route with OAuth authentication.
Requires a valid OAuth token. Individual tool scope checking
is handled by the tool registry in `tools/call-tool`."
["/mcp"
{:post {:no-doc true
:mcp-auth true
:handler mcp-handler}}])
Step 4: Update middleware to trigger on :mcp-auth instead of :required-scope
In src/com/getorcha/link/mcp/middleware.clj, update oauth-auth-middleware (line ~194-200) to compile on :mcp-auth route data instead of :required-scope:
(def oauth-auth-middleware
"Compiled Reitit middleware for OAuth access token validation.
Only activates on routes with `:mcp-auth` in route data.
Extracts Bearer token, validates JWT, and adds :oauth-claims to the request.
Usage: [oauth-auth-middleware config]
Config keys:
:kms-client - KmsClient instance
:key-arn - KMS key ARN
:issuer - expected issuer"
{:name ::oauth-auth
:compile
(fn [{:keys [mcp-auth]} _opts]
(when mcp-auth
{:wrap (fn [handler config]
(wrap-oauth-auth handler config))}))})
Remove require-scope-middleware entirely from the middleware definition (it's no longer needed at the route level). Also remove it from the middleware chain in src/com/getorcha/link/http.clj:103:
;; Remove this line:
mcp.middleware/require-scope-middleware
Update the comment at line 78:
;; MCP transport (OAuth authenticated)
Step 5: Run tests
Run: clj -X:test:silent :nses '[com.getorcha.link.mcp-test]' 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: Tests still fail because tool registrations still use "mcp:read" scope. Next task fixes that.
Step 6: Commit
git add src/com/getorcha/link/oauth/flow.clj src/com/getorcha/link/oauth/http.clj src/com/getorcha/link/mcp/http.clj src/com/getorcha/link/mcp/middleware.clj src/com/getorcha/link/http.clj
git commit -m "refactor: remove transport-level scope gate, use per-tool scope checking"
Files:
src/com/getorcha/link/mcp/tools/list_documents.clj:123src/com/getorcha/link/mcp/tools/get_document.clj:162src/com/getorcha/link/mcp/tools/search_documents.clj:76src/com/getorcha/link/mcp/tools/get_line_items.clj:117src/com/getorcha/link/mcp/tools.clj:50 (default scope)Step 1: Update all tool registrations
In each of the four tool files, change :scope "mcp:read" to :scope "docs:read".
In src/com/getorcha/link/mcp/tools.clj:50, update the default scope:
(not (:scope tool)) (assoc :scope "docs:read")
Also update the docstring at line 12 to say "docs:read" instead of "mcp:read".
Step 2: Run tests
Run: clj -X:test:silent :nses '[com.getorcha.link.mcp-test]' 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: All existing tests pass. The test helper now grants "docs:read" (among others), tools require "docs:read", and the route no longer gates on a specific scope.
Step 3: Lint
Run: clj-kondo --lint src test dev
Fix any issues.
Step 4: Commit
git add src/com/getorcha/link/mcp/tools/list_documents.clj src/com/getorcha/link/mcp/tools/get_document.clj src/com/getorcha/link/mcp/tools/search_documents.clj src/com/getorcha/link/mcp/tools/get_line_items.clj src/com/getorcha/link/mcp/tools.clj
git commit -m "refactor: migrate tool scopes from mcp:read to docs:read"
Files:
resources/migrations/YYYYMMDDHHMMSS-add-fpna-data-map.up.sqlresources/migrations/YYYYMMDDHHMMSS-add-fpna-data-map.down.sqlStep 1: Generate migration timestamp
Run: bb migrate create "add-fpna-data-map"
This creates the migration files with the correct timestamp prefix.
Step 2: Write the up migration
Write to the generated .up.sql file:
CREATE TABLE fpna_data_map (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
legal_entity_id uuid NOT NULL REFERENCES legal_entity(id),
protocol_version text NOT NULL,
status text NOT NULL CHECK (status IN ('draft', 'ready')),
content text NOT NULL,
last_reviewed timestamptz,
reviewed_by text,
created_by uuid NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
--;;
CREATE INDEX idx_fpna_data_map_latest
ON fpna_data_map (legal_entity_id, created_at DESC);
Step 3: Write the down migration
Write to the generated .down.sql file:
DROP INDEX IF EXISTS idx_fpna_data_map_latest;
--;;
DROP TABLE IF EXISTS fpna_data_map;
Step 4: Verify migration runs
If you have a running REPL with Integrant system, the migration runs on startup. Otherwise verify the SQL is syntactically valid:
Run: psql -h localhost -U postgres -d orcha -c "\d fpna_data_map" (after system restart)
Step 5: Commit
git add resources/migrations/*add-fpna-data-map*
git commit -m "feat: add fpna_data_map table for Data Map storage"
Files:
resources/com/getorcha/controlling/data-discovery-protocol.mdStep 1: Copy the DDP document to classpath resources
mkdir -p resources/com/getorcha/controlling
cp docs/controlling/data-discovery-protocol.md resources/com/getorcha/controlling/data-discovery-protocol.md
Step 2: Verify it's loadable from classpath
In a REPL (@clojure-eval):
(slurp (clojure.java.io/resource "com/getorcha/controlling/data-discovery-protocol.md"))
Expected: Returns the markdown content as a string.
Step 3: Commit
git add resources/com/getorcha/controlling/data-discovery-protocol.md
git commit -m "feat: add DDP protocol doc as classpath resource"
Both tools need to resolve legal_entity_id from the context. This shared logic goes in the queries namespace since it's a data access concern.
Files:
src/com/getorcha/link/queries/documents.clj (add function after legal-entity-ids-for-identity)Step 1: Write the failing test
Create or extend a test. Since this is a query helper, test it through the tool integration tests in Task 8. For now, just implement it — it's simple enough that the tool tests will cover it.
Step 2: Add the resolution function
Add after legal-entity-ids-for-identity (line ~78) in src/com/getorcha/link/queries/documents.clj:
(defn resolve-legal-entity-id
"Resolves a legal entity ID from an optional input and the identity's allowed set.
Three cases:
1. `legal-entity-id` provided and in `allowed-ids` → returns it
2. `legal-entity-id` nil, `allowed-ids` has exactly one → returns it
3. Otherwise → returns {:error \"message\"}
Arguments:
- legal-entity-id: UUID or nil
- allowed-ids: set of UUIDs from `legal-entity-ids-for-identity`"
[legal-entity-id allowed-ids]
(cond
;; Provided and valid
(and legal-entity-id (contains? allowed-ids legal-entity-id))
legal-entity-id
;; Provided but not accessible
legal-entity-id
{:error "Legal entity not accessible to this identity."}
;; Not provided, single LE
(= 1 (count allowed-ids))
(first allowed-ids)
;; Not provided, multiple LEs
(< 1 (count allowed-ids))
{:error (str "Multiple legal entities available. Use orcha-master-data-legal-entities "
"to ask the user which legal entity they are referring to.")}
;; Not provided, no LEs
:else
{:error "No legal entities accessible to this identity."}))
Step 3: Commit
git add src/com/getorcha/link/queries/documents.clj
git commit -m "feat: add legal entity ID resolution helper"
orcha-fpna-context ToolFiles:
src/com/getorcha/link/mcp/tools/fpna_context.cljsrc/com/getorcha/link/mcp/tools.clj:154-159 (add require)Step 1: Write the tool
Create src/com/getorcha/link/mcp/tools/fpna_context.clj:
(ns com.getorcha.link.mcp.tools.fpna-context
"MCP tool for retrieving FP&A data discovery context.
Called at session start to determine the state of data discovery for a
legal entity. Returns the DDP protocol document, a partial Data Map
draft, or a completed Data Map depending on the current state."
(:require [cheshire.core :as json]
[clojure.java.io :as io]
[com.getorcha.db.sql :as db.sql]
[com.getorcha.link.mcp.tools :as tools]
[com.getorcha.link.queries.documents :as queries]))
(defn ^:private load-ddp-protocol
"Loads the Data Discovery Protocol document from classpath."
^String []
(slurp (io/resource "com/getorcha/controlling/data-discovery-protocol.md")))
(defn ^:private get-latest-data-map
"Returns the latest fpna_data_map row for a legal entity, or nil."
[db-pool legal-entity-id]
(db.sql/execute-one!
db-pool
{:select [:id :protocol-version :status :content :last-reviewed :reviewed-by :created-at]
:from [:fpna-data-map]
:where [:= :legal-entity-id legal-entity-id]
:order-by [[:created-at :desc]]
:limit 1}))
(defn ^:private handle-fpna-context
"Handler for orcha-fpna-context tool.
Arguments:
- legal_entity_id: UUID of the legal entity (optional, auto-resolved for single-LE identities)"
[args {:keys [db-pool legal-entity-ids] :as _context}]
(let [le-id-str (:legal_entity_id args)
le-id (when le-id-str
(try (parse-uuid le-id-str)
(catch Exception _ nil)))]
(if (and le-id-str (nil? le-id))
{:isError true
:content [{:type "text"
:text (json/generate-string
{:error "Invalid legal_entity_id format. Expected UUID."})}]}
(let [resolved (queries/resolve-legal-entity-id le-id legal-entity-ids)]
(if (map? resolved)
{:isError true
:content [{:type "text"
:text (json/generate-string resolved)}]}
(let [data-map (get-latest-data-map db-pool resolved)]
{:content [{:type "text"
:text (json/generate-string
(cond
;; No data map exists — boot
(nil? data-map)
{:status "boot"
:legal_entity_id (str resolved)
:content (load-ddp-protocol)}
;; Draft — return protocol + partial data map
(= "draft" (:fpna-data-map/status data-map))
{:status "draft"
:legal_entity_id (str resolved)
:protocol_version (:fpna-data-map/protocol-version data-map)
:content {:protocol (load-ddp-protocol)
:data_map (:fpna-data-map/content data-map)}}
;; Ready — return completed data map only
:else
{:status "ready"
:legal_entity_id (str resolved)
:protocol_version (:fpna-data-map/protocol-version data-map)
:content (:fpna-data-map/content data-map)}))}]}))))))
(tools/register-tool!
{:name "orcha-fpna-context"
:description "Get the current state of financial data discovery for a legal entity. Returns one of three statuses: 'boot' (no Data Map exists, returns the discovery protocol), 'draft' (partial Data Map from a previous session, returns protocol + draft), or 'ready' (reviewed Data Map for downstream use). Call this at session start before any FP&A work."
:inputSchema {:type "object"
:properties {"legal_entity_id" {:type "string"
:description "UUID of the legal entity. Optional if the identity has access to only one legal entity."
:format "uuid"}}
:required []}
:handler handle-fpna-context
:scope "fpna:read"})
Step 2: Register in tool init
In src/com/getorcha/link/mcp/tools.clj, add to init-tools! (after line 157):
(require 'com.getorcha.link.mcp.tools.fpna-context)
Step 3: Lint
Run: clj-kondo --lint src/com/getorcha/link/mcp/tools/fpna_context.clj
Step 4: Commit
git add src/com/getorcha/link/mcp/tools/fpna_context.clj src/com/getorcha/link/mcp/tools.clj
git commit -m "feat: implement orcha-fpna-context MCP tool"
orcha-fpna-save-dm ToolFiles:
src/com/getorcha/link/mcp/tools/fpna_save_dm.cljsrc/com/getorcha/link/mcp/tools.clj (add require)Step 1: Write the tool
Create src/com/getorcha/link/mcp/tools/fpna_save_dm.clj:
(ns com.getorcha.link.mcp.tools.fpna-save-dm
"MCP tool for persisting FP&A Data Maps.
Inserts a new row into the fpna_data_map table (append-only history).
Each save is a full document replacement — the agent sends the complete
markdown content every time."
(:require [cheshire.core :as json]
[com.getorcha.db.sql :as db.sql]
[com.getorcha.link.mcp.tools :as tools]
[com.getorcha.link.queries.documents :as queries]))
(defn ^:private handle-fpna-save-dm
"Handler for orcha-fpna-save-dm tool.
Arguments:
- legal_entity_id: UUID (optional, auto-resolved for single-LE identities)
- content: markdown string (required)
- protocol_version: string (required)
- status: 'draft' or 'ready' (required)
- last_reviewed: ISO 8601 timestamp (optional)
- reviewed_by: string (optional)"
[args {:keys [db-pool identity-id legal-entity-ids] :as _context}]
(let [le-id-str (:legal_entity_id args)
le-id (when le-id-str
(try (parse-uuid le-id-str)
(catch Exception _ nil)))]
(if (and le-id-str (nil? le-id))
{:isError true
:content [{:type "text"
:text (json/generate-string
{:error "Invalid legal_entity_id format. Expected UUID."})}]}
(let [resolved (queries/resolve-legal-entity-id le-id legal-entity-ids)]
(if (map? resolved)
{:isError true
:content [{:type "text"
:text (json/generate-string resolved)}]}
(let [row (db.sql/execute-one!
db-pool
{:insert-into :fpna-data-map
:values [(cond-> {:legal-entity-id resolved
:protocol-version (:protocol_version args)
:status (:status args)
:content (:content args)
:created-by identity-id}
(:last_reviewed args)
(assoc :last-reviewed [:cast (:last_reviewed args) :timestamptz])
(:reviewed_by args)
(assoc :reviewed-by (:reviewed_by args)))]
:returning [:id :legal-entity-id :status :created-at]})]
{:content [{:type "text"
:text (json/generate-string
{:id (str (:fpna-data-map/id row))
:legal_entity_id (str (:fpna-data-map/legal-entity-id row))
:status (:fpna-data-map/status row)
:created_at (str (:fpna-data-map/created-at row))})}]}))))))
(tools/register-tool!
{:name "orcha-fpna-save-dm"
:description "Save or update the Data Map for a legal entity. Call with status 'draft' after completing each domain section to preserve progress. Set status to 'ready' only after the human confirms the map is complete. Each call overwrites the previous version (the full markdown content must be provided every time)."
:inputSchema {:type "object"
:properties {"legal_entity_id" {:type "string"
:description "UUID of the legal entity. Optional if the identity has access to only one legal entity."
:format "uuid"}
"content" {:type "string"
:description "Full Data Map markdown content"}
"protocol_version" {:type "string"
:description "Version of the DDP protocol used (e.g., '1.0')"}
"status" {:type "string"
:description "Data Map status"
:enum ["draft" "ready"]}
"last_reviewed" {:type "string"
:description "ISO 8601 timestamp of last human review"
:format "date-time"}
"reviewed_by" {:type "string"
:description "Name or identifier of the reviewer"}}
:required ["content" "protocol_version" "status"]}
:handler handle-fpna-save-dm
:scope "fpna:write"})
Step 2: Register in tool init
In src/com/getorcha/link/mcp/tools.clj, add to init-tools! (after the fpna-context require):
(require 'com.getorcha.link.mcp.tools.fpna-save-dm)
Step 3: Lint
Run: clj-kondo --lint src/com/getorcha/link/mcp/tools/fpna_save_dm.clj
Step 4: Commit
git add src/com/getorcha/link/mcp/tools/fpna_save_dm.clj src/com/getorcha/link/mcp/tools.clj
git commit -m "feat: implement orcha-fpna-save-dm MCP tool"
Files:
test/com/getorcha/link/mcp_test.cljStep 1: Write context boot test
Add to test/com/getorcha/link/mcp_test.clj, after the existing tool tests:
;; FP&A Context Tool Tests
;; -----------------------------------------------------------------------------
(deftest test-fpna-context-boot
(testing "fpna-context returns boot status when no data map exists"
(mcp.tools/init-tools!)
(let [{:keys [identity legal-entity]} (create-test-identity-with-legal-entity! fixtures/*db*)
token (get-test-access-token fixtures/*db* identity)
response (mcp-request {:jsonrpc "2.0"
:method "tools/call"
:params {:name "orcha-fpna-context"
:arguments {"legal_entity_id" (str (:legal-entity/id legal-entity))}}
:id 1}
token)]
(is (= 200 (:status response)))
(let [content (get-in response [:body :result :content 0 :text])
result (json/parse-string content true)]
(is (= "boot" (:status result)))
(is (= (str (:legal-entity/id legal-entity)) (:legal_entity_id result)))
;; Content should be the DDP protocol document
(is (str/includes? (:content result) "Data Discovery Protocol"))))))
Step 2: Write context auto-resolve test
(deftest test-fpna-context-auto-resolve-single-le
(testing "fpna-context auto-resolves legal entity for single-LE identity"
(mcp.tools/init-tools!)
(let [{:keys [identity legal-entity]} (create-test-identity-with-legal-entity! fixtures/*db*)
token (get-test-access-token fixtures/*db* identity)
response (mcp-request {:jsonrpc "2.0"
:method "tools/call"
:params {:name "orcha-fpna-context"
:arguments {}}
:id 1}
token)]
(is (= 200 (:status response)))
(let [content (get-in response [:body :result :content 0 :text])
result (json/parse-string content true)]
(is (= "boot" (:status result)))
(is (= (str (:legal-entity/id legal-entity)) (:legal_entity_id result)))))))
Step 3: Write context draft test
(deftest test-fpna-context-draft
(testing "fpna-context returns draft with protocol + data map"
(mcp.tools/init-tools!)
(let [{:keys [identity legal-entity]} (create-test-identity-with-legal-entity! fixtures/*db*)
le-id (:legal-entity/id legal-entity)
;; Insert a draft data map directly
_ (db.sql/execute-one!
fixtures/*db*
{:insert-into :fpna-data-map
:values [{:legal-entity-id le-id
:protocol-version "1.0"
:status "draft"
:content "# Data Map: Test Entity\n\n## Budget\nPartial..."
:created-by (:identity/id identity)}]})
token (get-test-access-token fixtures/*db* identity)
response (mcp-request {:jsonrpc "2.0"
:method "tools/call"
:params {:name "orcha-fpna-context"
:arguments {"legal_entity_id" (str le-id)}}
:id 1}
token)]
(is (= 200 (:status response)))
(let [content (get-in response [:body :result :content 0 :text])
result (json/parse-string content true)]
(is (= "draft" (:status result)))
(is (= "1.0" (:protocol_version result)))
;; Content should have both protocol and data_map
(is (str/includes? (get-in result [:content :protocol]) "Data Discovery Protocol"))
(is (str/includes? (get-in result [:content :data_map]) "Budget"))))))
Step 4: Write context ready test
(deftest test-fpna-context-ready
(testing "fpna-context returns ready with data map only"
(mcp.tools/init-tools!)
(let [{:keys [identity legal-entity]} (create-test-identity-with-legal-entity! fixtures/*db*)
le-id (:legal-entity/id legal-entity)
_ (db.sql/execute-one!
fixtures/*db*
{:insert-into :fpna-data-map
:values [{:legal-entity-id le-id
:protocol-version "1.0"
:status "ready"
:content "# Data Map: Test Entity\n\nComplete map..."
:created-by (:identity/id identity)}]})
token (get-test-access-token fixtures/*db* identity)
response (mcp-request {:jsonrpc "2.0"
:method "tools/call"
:params {:name "orcha-fpna-context"
:arguments {"legal_entity_id" (str le-id)}}
:id 1}
token)]
(is (= 200 (:status response)))
(let [content (get-in response [:body :result :content 0 :text])
result (json/parse-string content true)]
(is (= "ready" (:status result)))
;; Content should be just the markdown string, not an object
(is (string? (:content result)))
(is (str/includes? (:content result) "Complete map"))))))
Step 5: Write context access denied test
(deftest test-fpna-context-access-denied
(testing "fpna-context denies access to other legal entities"
(mcp.tools/init-tools!)
(let [{:keys [identity]} (create-test-identity-with-legal-entity! fixtures/*db*)
other-tenant (db.sql/execute-one!
fixtures/*db*
{:insert-into :tenant
:values [{:name "Other Tenant FPNA" :slug (str "other-fpna-" (random-uuid))}]
:returning [:*]})
other-le (db.sql/execute-one!
fixtures/*db*
{:insert-into :legal-entity
:values [{:name "Other LE FPNA"
:tenant-id (:tenant/id other-tenant)}]
:returning [:*]})
token (get-test-access-token fixtures/*db* identity)
response (mcp-request {:jsonrpc "2.0"
:method "tools/call"
:params {:name "orcha-fpna-context"
:arguments {"legal_entity_id" (str (:legal-entity/id other-le))}}
:id 1}
token)]
(is (= 200 (:status response)))
(let [result (get-in response [:body :result])
text (get-in result [:content 0 :text])]
(is (:isError result))
(is (str/includes? text "not accessible"))))))
Step 6: Run tests
Run: clj -X:test:silent :nses '[com.getorcha.link.mcp-test]' 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: All tests pass.
Step 7: Commit
git add test/com/getorcha/link/mcp_test.clj
git commit -m "test: add integration tests for orcha-fpna-context tool"
Files:
test/com/getorcha/link/mcp_test.cljStep 1: Write save-dm basic test
;; FP&A Save Data Map Tool Tests
;; -----------------------------------------------------------------------------
(deftest test-fpna-save-dm
(testing "fpna-save-dm persists a data map"
(mcp.tools/init-tools!)
(let [{:keys [identity legal-entity]} (create-test-identity-with-legal-entity! fixtures/*db*)
token (get-test-access-token fixtures/*db* identity)
response (mcp-request {:jsonrpc "2.0"
:method "tools/call"
:params {:name "orcha-fpna-save-dm"
:arguments {"legal_entity_id" (str (:legal-entity/id legal-entity))
"content" "# Data Map: Test\n\n## Budget\nDraft content"
"protocol_version" "1.0"
"status" "draft"}}
:id 1}
token)]
(is (= 200 (:status response)))
(let [content (get-in response [:body :result :content 0 :text])
result (json/parse-string content true)]
(is (some? (:id result)))
(is (= (str (:legal-entity/id legal-entity)) (:legal_entity_id result)))
(is (= "draft" (:status result)))
(is (some? (:created_at result)))))))
Step 2: Write save-then-context roundtrip test
(deftest test-fpna-save-then-context-roundtrip
(testing "saving a draft data map then querying context returns it"
(mcp.tools/init-tools!)
(let [{:keys [identity legal-entity]} (create-test-identity-with-legal-entity! fixtures/*db*)
le-id-str (str (:legal-entity/id legal-entity))
token (get-test-access-token fixtures/*db* identity)
;; Save a draft
_save (mcp-request {:jsonrpc "2.0"
:method "tools/call"
:params {:name "orcha-fpna-save-dm"
:arguments {"legal_entity_id" le-id-str
"content" "# Data Map v1"
"protocol_version" "1.0"
"status" "draft"}}
:id 1}
token)
;; Query context
ctx-resp (mcp-request {:jsonrpc "2.0"
:method "tools/call"
:params {:name "orcha-fpna-context"
:arguments {"legal_entity_id" le-id-str}}
:id 2}
token)]
(is (= 200 (:status ctx-resp)))
(let [content (get-in ctx-resp [:body :result :content 0 :text])
result (json/parse-string content true)]
(is (= "draft" (:status result)))
(is (= "# Data Map v1" (get-in result [:content :data_map])))))))
Step 3: Write history test (latest wins)
(deftest test-fpna-save-dm-history-latest-wins
(testing "context returns the latest saved version"
(mcp.tools/init-tools!)
(let [{:keys [identity legal-entity]} (create-test-identity-with-legal-entity! fixtures/*db*)
le-id-str (str (:legal-entity/id legal-entity))
token (get-test-access-token fixtures/*db* identity)
;; Save v1
_ (mcp-request {:jsonrpc "2.0"
:method "tools/call"
:params {:name "orcha-fpna-save-dm"
:arguments {"legal_entity_id" le-id-str
"content" "# v1"
"protocol_version" "1.0"
"status" "draft"}}
:id 1}
token)
;; Save v2
_ (mcp-request {:jsonrpc "2.0"
:method "tools/call"
:params {:name "orcha-fpna-save-dm"
:arguments {"legal_entity_id" le-id-str
"content" "# v2 - updated"
"protocol_version" "1.0"
"status" "draft"}}
:id 2}
token)
;; Context should return v2
ctx-resp (mcp-request {:jsonrpc "2.0"
:method "tools/call"
:params {:name "orcha-fpna-context"
:arguments {"legal_entity_id" le-id-str}}
:id 3}
token)]
(let [content (get-in ctx-resp [:body :result :content 0 :text])
result (json/parse-string content true)]
(is (= "draft" (:status result)))
(is (= "# v2 - updated" (get-in result [:content :data_map])))))))
Step 4: Run all tests
Run: clj -X:test:silent :nses '[com.getorcha.link.mcp-test]' 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: All tests pass.
Step 5: Lint everything
Run: clj-kondo --lint src test dev
Fix any issues.
Step 6: Commit
git add test/com/getorcha/link/mcp_test.clj
git commit -m "test: add integration tests for orcha-fpna-save-dm tool"
Step 1: Run the full test suite
Run: clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Execution error|failed because|Ran .* tests)"
Expected: All tests pass, including tests from other namespaces that aren't affected by our changes.
Step 2: Full lint check
Run: clj-kondo --lint src test dev
Expected: No warnings or errors.
Step 3: Verify scope changes didn't break anything
Confirm that:
discovery.clj lists the new scopesdocs:* tools use docs:readfpna:read/fpna:write/mcp route uses :mcp-auth true instead of :required-scopedocs:readStep 4: Commit (if any fixes were needed)
git commit -m "fix: address issues found in final verification"