Technical notes

Running the connector in Claude

The connector is a local MCP server over stdio. Claude Code and Claude Desktop both start it as a child process; it talks to the local bank over HTTP and HTTPS and to its key helper over stdio.

Before Claude: the bank and the build

npm install
bank/scripts/pf-up.sh                     # PingFederate with the modules (6-8 minutes the first time)
node bank/scripts/federation-up.mjs enrol # the Trust Anchor, and PingFederate + the Trust Mark Issuer as its subordinates
bank/scripts/services.sh start            # attester, identity provider, Trust Mark Issuer, bank API, PDP, partner
npm run build                             # dist/connector.mjs

bank/scripts/services.sh status shows what is up. npm run e2e proves the whole path with software keys before a person is involved.

Keys

Backend Set up Chosen when
YubiKey (yubikey-piv) python3 -m venv helpers/yk-signer/.venv && helpers/yk-signer/.venv/bin/pip install yubikey-manager==5.9.2, then once, by the YubiKey's owner: change the factory PIN and PUK (helpers/yk-signer/.venv/bin/ykman piv access change-pin, change-puk) and protect the management key (helpers/yk-signer/.venv/bin/ykman piv access change-management-key --generate --protect). ykman lives in the venv, not on your PATH a set-up YubiKey is plugged in, or backend: "yubikey-piv"
Secure Enclave (secure-enclave) make -C helpers/se-signer bundle (Xcode, team JH6RX4DRG2; Node.js's official build at NODE_BINARY, default the nvm 20.19.3 one). On macOS 27 the bank gets App Attest evidence; on 26 and earlier it records the keys as self-asserted no set-up YubiKey is plugged in, or backend: "secure-enclave"
Software tests only: CONNECTOR_ALLOW_SOFTWARE_KEYS=1 and CONNECTOR_KEY_BACKEND=software never by default

Once an agent has keys, it stays with that backend until bank_reset.

The YubiKey helper keeps its keys in PIV slots 82, 83 and 84 and will not replace a key it did not create. It asks for the PIN in its own macOS dialog; never type a PIN into Claude.

Claude Code

.mcp.json at the root of this repository registers the connector for this project:

{
  "mcpServers": {
    "claude-bank-connector": {
      "command": "node",
      "args": ["dist/connector.mjs"],
      "env": { "FEDCONN_INSECURE_LOCALHOST": "1" }
    }
  }
}

Start claude in the repository, approve the project's MCP server when asked, and /mcp lists its nine tools.

Claude Desktop

Claude Desktop starts MCP servers with a minimal environment, so every path is absolute. In ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "claude-bank-connector": {
      "command": "/opt/homebrew/bin/node",
      "args": ["/Users/davidhyland/Source/idp-fed-agentic-connector/dist/connector.mjs"],
      "env": { "FEDCONN_INSECURE_LOCALHOST": "1" }
    }
  }
}

Restart Claude Desktop. Use command -v node for the first path if Node lives elsewhere.

Run the connector in one Claude app at a time: both share the same state directory and the loopback port.

The demo

Before starting: in the browser, PingFederate's page at https://localhost:9131 carries a self-signed certificate, so the browser warns once - in Chrome, Advanced, then Proceed to localhost. The passkey you create is saved for localhost in whatever you choose (iCloud Keychain with Touch ID, or a security key); delete it afterwards if you like. The test customer's password is in the rig:

grep TF_VAR_test_user_password ~/Source/pf-agentic-identity-connector/conformance/secrets.env
  1. "Enrol my bank agent." Claude calls bank_enrol and a browser tab opens at the bank's sign-in page. Create a passkey (any username), then approve Touch ID when the Secure Enclave helper asks - that is the agent's own key proving itself. Claude polls bank_status until the stage is enrolled.
  2. "Let it read my accounts." bank_authorize opens PingFederate: sign in as suite-user and approve. Touch ID again, at most once every five minutes.
  3. "What are my balances and recent transactions?" bank_get_accounts and bank_get_transactions; each answer says who the token names as the customer (sub) and which agent acted (act).
  4. "Any offers from the bank's partners?" The partner is a separate mandate: bank_authorize with audience: "partner", approve, then partner_get_offers. The mandate states its purpose - personalised benefits - and the answer shows how the partner decided: its enforcement point, configured by the federation, asked the federation's layer and then its own PDP, and each returns its checks.
  5. "How does the federation see you?" bank_explain_trust shows the agent's mission three ways - what it declares, what the bank vouches for and caps, and what every member resolves - then resolves its chain to the Trust Anchor.
  6. "Could you use those offers for marketing?" Ask Claude to get a partner mandate for DirectMarketing. The bank's PDP refuses it before any consent page (purpose_outside_mission in bank/.run/pdp.log). Try AccountManagement and the bank refuses again - within the agent's mission, but not a purpose the partner accepts (purpose_not_accepted).

The finale is the bank withdrawing its trust. Take the agent's entity id from bank_status, then, as the bank's operator:

curl -s -X POST http://localhost:7400/admin/revoke -H "authorization: Bearer $(cat bank/.secrets/authority-admin.token)" -H 'content-type: application/json' -d '{"sub":"<entity id>"}'

Ask for the partner's offers again: the federation's layer refuses (trust_mark_inactive), while the bank's own API still answers. Then revoke the agent itself:

curl -sk -X DELETE "<entity id>" -H "authorization: Bearer $(cat bank/.secrets/authority-admin.token)"

Now the partner's layer cannot resolve the agent at all (not_in_federation), and the bank's authorization server refuses its next request with invalid_client. bank_reset (with confirm: true) and a fresh bank_enrol start again.

Keep PingFederate running for the whole demo: its registry of hosted agents is in memory, so a restart forgets the agent.

Settings

All optional. Defaults point at this repository's local bank.

Variable Default What it does
CONNECTOR_KEY_BACKEND auto secure-enclave, yubikey-piv, or software (tests)
CONNECTOR_STATE_DIR ~/Library/Application Support/idp-fed-agentic-connector the agent's state, 0600 files
CONNECTOR_BROWSER open none only reports the URL (tests, headless)
CONNECTOR_INSTANCE_GATE presence Secure Enclave inst key: presence, biometry or none
CONNECTOR_INSTANCE_PROTECTION when-unlocked or after-first-unlock
CONNECTOR_SCOPE_BANK, CONNECTOR_SCOPE_PARTNER accounts:read transactions:read, offers:read what each mandate asks for
FEDCONN_INSECURE_LOCALHOST off 1 skips certificate checks for loopback hosts only - PingFederate's self-signed listener. FEDCONN_CA_FILE=<pem> trusts a specific certificate instead, and is better
FEDCONN_HOME found from the bundle's location where helpers/ lives, if the bundle is moved
BANK_ISSUER, BANK_ENROLMENT_URL, IDP_ISSUER, BANK_API, PARTNER_RS, TRUST_ANCHOR the local bank point the connector at another bank

When something goes wrong

The extension, and other people's Macs

make -C helpers/se-signer mcpb packs the signed app as a Claude Desktop extension, build/ClaudeBankConnector.mcpb, pointed at the Railway demo's bank: double-click it and Claude Desktop installs it. Claude Desktop runs the extension's server/index.js with its own Node.js; that starts the app's own signed node on the sealed connector.mjs - the only caller the helper accepts - with NODE_OPTIONS stripped, and registers the app with LaunchServices so App Attest answers. The demo site offers the file at /download/ClaudeBankConnector.mcpb and explains it at /connect.

Signed with the team's Apple Development certificate, the build runs on Macs registered to the team and nowhere else. make -C helpers/se-signer release makes the one for anyone's Mac - a Release build, sealed with the team's Developer ID with the hardened runtime and no entitlements (App Attest answers without any; the Xcode build's get-task-allow is what notarisation refuses), notarised, stapled, packed - once two things the account holder has to make are on this Mac:

  1. A Developer ID Application certificate for team JH6RX4DRG2, in the login keychain. Xcode → Settings → Accounts → the team → Manage Certificates → + → Developer ID Application (only the Account Holder can create one); or developer.apple.com → Certificates → + → Developer ID Application, then open the downloaded .cer. security find-identity -v -p codesigning should then list it; the Makefile picks it up by itself.

  2. Notarisation credentials, stored once as a keychain profile named fedconn-notary, with your own Apple ID and an app-specific password from appleid.apple.com (or an App Store Connect API key):

    xcrun notarytool store-credentials fedconn-notary --apple-id you@example.com --team-id JH6RX4DRG2
    

    It prompts for the app-specific password and keeps it in the keychain; nothing lands in the repo.

Then make -C helpers/se-signer release and deploy/railway/deploy.sh demo to publish the new file. Node.js's binary keeps its own Developer ID signature (team HX7739G8FX, hardened runtime), which is what notarisation wants of nested code and what the helper checks its caller against. Untested until the certificate exists: whether App Attest answers under a Developer ID signature; it does under a development one with the hardened runtime and no entitlements, which is the same shape.

Without a Mac at all, the same connector is served to Claude.ai, ChatGPT and Claude Code from the demo site: https://idp-fedconn-demo.up.railway.app/mcp, with its keys on the server (the demo page's connector, over MCP).

Symptom Cause Fix
E_LOCKED the Mac is locked and the Secure Enclave inst key is when-unlocked unlock the Mac
E_SETUP_REQUIRED from the YubiKey its PIV management key is not PIN-protected the one-time management-key command above, if you are happy to change it
E_KEY_EXISTS PIV slot 82, 83 or 84 holds a key this agent did not create - often its own keys from before a bank_reset on firmware 5.7 or later, helpers/yk-signer/.venv/bin/ykman piv keys delete <slot>; older YubiKeys cannot delete PIV keys, so enrol again with replace_yubikey_keys: true once you are sure nothing else uses those slots
appAttest.supported: false macOS 26 or earlier (App Attest arrived in macOS 27), or a copy of the app LaunchServices has not registered upgrade, or lsregister -f the app (make bundle does); on macOS 26 the bank attests the key as self-asserted
E_CALLER_UNVERIFIED the helper inside ClaudeBankConnector.app was asked by something other than the app's own node running its sealed connector.mjs, or the app's seal is broken run the connector as the app's Contents/MacOS/node Contents/Resources/connector.mjs, with no NODE_OPTIONS; rebuild the app if it was edited
invalid_client ... revoked at this authority the bank revoked this agent bank_reset, then enrol again
partner refuses after a PingFederate restart the hosted-entity registry is in memory, so the agent's federation entity is gone enrol again (a durable registry fixes this)
the loopback URL fails port 53682 is taken, often by the other Claude app running the connector run it in one app at a time