TL;DR — read this first
/mcp, speak Streamable-HTTP,
and sit behind Keystone OAuth 2.1. The Portal deliberately has no MCP — it's the human UI.
🧩 What it is
A bridge that copies matters/clients out of on-prem Aderant and creates them as work items in Sharedo / Clio Operate.
🖥️ How you use it
A web portal — and the same portal embedded inside Sharedo via an iframe widget with single sign-on.
🤖 The AI angle
MCP lets an AI agent (or a Sharedo workflow) search, preview, and import matters as tools — not just humans clicking.
A point-in-time health capture from 2026-06-14 is at the bottom of this page. It is not a live status.
1System context — the whole thing on one screen
Three people-facing entry points on the left, three services we own in the middle, and the systems we depend on on the right. Follow the colours: Portal Proxy SQL API External.
2The 4 building blocks (what each one actually does)
Think of it like a relay team. Each runner has exactly one job.
Portal AderantImportPortal
The face. The screens staff use to search matters, preview the transform, click Import, and see history. Holds no business secrets itself — it asks the Proxy to do the real work. Has no MCP by design.
Proxy ShareDoAderantAPIProxy
The brain + the writer. Holds tenant secrets, transforms Aderant data → Sharedo shape, and is the only thing that writes into Sharedo/Clio. Exposes the main MCP (7 tools, incl. the write tools).
SQL API AderantSQLAPITypeScript
The reader. The only thing allowed to touch the on-prem Aderant SQL Server. Read-only, column-whitelisted, parameterised. Exposes a read-only MCP (5 tools). Tunnelled out because the DB lives behind a firewall.
pms-sync-embed
The doorway. A Sharedo widget that iframes the Portal, mints a signed SSO token so the Sharedo user is logged in automatically, and brokers API calls back through Sharedo. Deployed via Foundry CLI, not Coolify.
3How one import actually flows
This is the money path. A user picks a matter; it ends up as a Sharedo work item. Read it top to bottom.
importWriteEnabled.4Does it use an MCP service? — Yes, two of them
Your direct question, answered with the code. Verified against source — not just the docs.
Proxy /mcp ✅
- Streamable-HTTP transport
- 7 tools (read + write)
- Scopes:
aderant-api-proxy:read / :write - Resolves tenant from token, then loads org secrets server-side
SQL API /mcp ✅
- Streamable-HTTP transport
- 5 tools (read-only)
- Scope:
pms-sql-api:read - Column-whitelisted, parameterised queries
Portal /mcp ❌
- No
@modelcontextprotocol/sdkdependency - It's the human UI shell
- Its capabilities are already exposed by the two backends — a 3rd MCP would just duplicate them
5How the MCP request actually works
Every MCP call is authenticated and scoped to one tenant before any tool runs. Here's the sequence.
| Aspect | Proxy /mcp | SQL API /mcp |
|---|---|---|
| Mount point | app.all('/mcp') · server.ts:217 | app.all('/mcp') · index.ts:82 |
| Discovery | /.well-known/oauth-protected-resource (both) | |
| Transport | StreamableHTTPServerTransport · SDK ^1.29.0 | |
| Token check | jose.jwtVerify against Keystone JWKS · checks issuer + audience | |
| Audience | aderant-api-proxy | pms-sql-api |
| Tenant | strictly from token claim → Supabase org config | stateless (read-only) |
| Tools are… | real — they call the service's own HTTP API in-process (not stubs) | |
6The actual MCP tools you can call
Proxy — 7 tools readwrite
| Tool | Scope | What it does |
|---|---|---|
get_status | public | Health of the proxy |
list_work_types | read | Sharedo work types in the caller's tenant |
search_matters | read | Search Aderant matters by name / code / client |
list_client_matters | read | All matters for a given client code |
preview_matter | read | Dry-run: what the import would produce (mapping, parties, dates) |
import_matter | write | Irreversible. Creates the work item + ODS parties. Needs scope + import_write_enabled + confirm:true |
reconcile_closures | read/write | Compare Aderant status vs Sharedo phase. apply:false = safe report; apply:true = closes drifted matters |
SQL API — 5 tools read-only
| Tool | Scope | What it does |
|---|---|---|
get_status | public | Health of the SQL agent |
list_tables | read | Discovered Aderant tables/views + their columns |
query_table | read | Read rows — columns whitelisted, values parameterised, paginated. Filter syntax like CLIENT_CODE=ABC;OPEN_DATE>2024-01-01 |
search_clients | read | Search clients by name/code |
search_matters | read | Search matters by name/code/client |
query_sql stub was deliberately removed as an anti-pattern — every read goes through whitelisted, parameterised access only.
7Auth & identity — three different doors, one identity provider
Everything ultimately trusts Keystone. But there are three distinct ways in, depending on who's knocking.
👩💼 Standalone user
Keystone OIDC. Browser → Keystone login → signed handoff JWT → portal verifies (KEYSTONE_HANDOFF_SECRET) → iron-session cookie with the user's real roles.
🧑⚖️ Embedded Sharedo user
Signed JWT handoff. Widget asks Proxy to mint a short-lived JWT → passed in the iframe URL #fragment → portal exchanges it for a session with roles:['embedded'] (no admin).
🤖 AI agent / automation
Keystone OAuth 2.1. Bearer token → /mcp verifies via JWKS, checks issuer + audience, enforces scopes & the write gates.
importWriteEnabled — a per-org / per-environment boolean in Supabase. If false, all write paths (UI, auto-import, and the MCP write tools) return 403. 2.
frame-ancestors / EMBED_ALLOWED_HOSTS — controls which Sharedo tenants are allowed to iframe the portal. A new tenant must be added here or the embed is CSP-blocked.
8Dependency map — the libraries each service leans on
Grouped by job, so you can scan for the one you care about.
| Job | Portal | Proxy | SQL API |
|---|---|---|---|
| Framework | next 15.5 · react 19 | express 4.18 | express 5.1 |
| MCP | — | @modelcontextprotocol/sdk ^1.29 | @modelcontextprotocol/sdk ^1.29 |
| Auth / JWT | jose 5 · iron-session 8 | jose 6 · express-basic-auth | jose 6 |
| Database | — | mssql 12 · axios-ntlm | mssql 10 |
| Tunnel | — | — | hyco-https (Azure Relay) |
| Data store | @supabase/supabase-js 2 | @supabase/supabase-js 2 | — |
| HTTP client | axios 1.9 | axios 1.16 · http-proxy-middleware | native fetch |
| Sandbox | — | quickjs-emscripten (validate transforms) | — |
| Logging | pino 9 | pino 10 · morgan | structured JSONL |
| Security mw | (Next headers/CSP) | helmet · cors · express-rate-limit | helmet · cors |
| Validation | zod 4 | zod (via MCP tools) | zod (via MCP tools) |
| AI / observability | langfuse 3 · @sentry/nextjs | — | — |
| API docs | — | swagger-jsdoc · swagger-ui | swagger-jsdoc · swagger-ui |
Widget (): built on @alterspective-engine/foundry ^0.6 + foundry-cli, bundled with tsup, requires Node ≥18. Pulls Foundry from GitHub Packages.
9External systems we depend on
If one of these is down, here's what breaks.
| System | Used by | For | If it's down… |
|---|---|---|---|
| 🔑 Keystone identity.alterspective.com.au | All four | Login (OIDC) + MCP token validation (OAuth 2.1) | Nobody can log in; MCP rejects all calls |
| 🗄️ Supabase | Portal, Proxy | Per-org config table aderant_org_configs (secrets, mappings, flags), import history | No org config → imports can't resolve tenant |
| ⚖️ Sharedo / Clio API per-tenant | Proxy, Widget | The write target: ODS parties, work items, participants, key dates, phase | Imports fail at the write step |
| 🏢 Aderant SQL Server on-prem, behind firewall | SQL API | The source of truth: clients, matters, parties | Nothing to read → previews/imports empty |
| 🌐 ngrok / Azure Relay | SQL API | Tunnel so the cloud can reach the on-prem SQL API | Cloud loses its link to on-prem data |
| 🧠 Synapse LLM synapse-api.alterspective.com.au | Portal | AI-assisted authoring of work-type mapping transforms (Synapse-only; OpenRouter fallback dropped) | AI mapping suggestions unavailable (imports still work manually) |
| 📊 Langfuse (optional) | Portal | Tracing & token-budget for the AI calls | Lose AI observability only |
| 📦 GitHub Packages | Widget (build) | Pulls @alterspective-engine/foundry | Widget can't build/deploy |
10Point-in-time health capture
Captured 2026-06-14 against production. This is a snapshot, not a live status.
| Service | Endpoint | Result |
|---|---|---|
| Portal | /api/health | 🟢 ok · v1.2.1 · production |
| Proxy | /health (note: not /api/health) | 🟢 ok · v1.1.0 |
| SQL API | /api/health/ready | 🟢 ready · DB connected to SHAREDO-PROXY (19ms) · 20 objects discovered |
End-to-end chain (Proxy /health/ready) | 🟢 db-proxy pass (147ms) · sharedo-api pass (302) · sharedo-identity pass (200) | |
"degraded" — that's benign. It only means Azure Relay isn't configured (it's running via ngrok instead), so the relay check returns warn. Readiness is green and the database is connected.
11One-page cheat sheet
🎯 The 30-second summary
- Bridge: Aderant (on-prem) → Sharedo/Clio (cloud)
- 4 parts: Widget · Portal · Proxy · SQL API
- Yes, it uses MCP — Proxy (7 tools) + SQL API (5 tools)
- Portal has no MCP — it's the human UI
- Proxy = only writer · SQL API = only reader
- All auth = Keystone
🔒 The safety gates
importWriteEnabledper org → blocks all writes- MCP writes need scope + flag + confirm:true
- SQL access is whitelisted & parameterised (no raw SQL)
- Embedded users get
roles:['embedded'](no admin) - CSP
frame-ancestorslimits who can iframe the portal
🌐 Production URLs
- Portal —
pms-sync.alterspective.com.au - Proxy —
aderant-proxy.alterspective.com.au - SQL API — tunnelled (ngrok / Azure Relay)
- Deploy — Coolify (push to main); widget via Foundry CLI
🤖 What an AI agent can do via MCP
- Read: search clients/matters, list tables, preview an import
- Write (gated): import a matter, reconcile closures
- Same capabilities a human has in the Portal — as tools