Note (2026-04-24): After this document was written, legal_entity was renamed to tenant and the old tenant was renamed to organization. Read references to these terms with the pre-rename meaning.

FP&A Data Map MCP Tools Implementation Plan

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


Task 1: OAuth Scope Refactor — Discovery & Validation

Files:

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"

Task 2: OAuth Scope Refactor — Flow Default & Route Gate

Files:

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"

Task 3: OAuth Scope Refactor — Update Existing Tool Scopes

Files:

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"

Task 4: Database Migration

Files:

Step 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"

Task 5: DDP Protocol Document as Classpath Resource

Files:

Step 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:

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"

Task 7: Implement orcha-fpna-context Tool

Files:

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"

Task 8: Implement orcha-fpna-save-dm Tool

Files:

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"

Task 9: Integration Tests — Context Tool

Files:

Step 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"

Task 10: Integration Tests — Save Tool & History

Files:

Step 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"

Task 11: Final Verification

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:

Step 4: Commit (if any fixes were needed)

git commit -m "fix: address issues found in final verification"