2026-06-09 19:25:24 -04:00
# sh-mcp
2026-07-06 17:41:18 -04:00
[](https://github.com/Sea-Haven-Industries/sh-mcp/actions/workflows/ci.yaml)




2026-06-09 19:25:24 -04:00
Sea Haven MCP platform. A TypeScript monorepo of trust-tiered **MCP servers** that expose
Sea Haven's proprietary integrations as tools, plus the **Cognito/Google auth broker** and the
**rebuilt scheduled jobs**. This service replaces `seahaven-slack-bot` and `exec-aide` , which are
deprecated completely; the conversational surface becomes a configurable Slack task agent.
Phase 2a: Cognito auth substrate (CDK) + pre-token & group-sync Lambdas (#4)
* Phase 2a: Cognito auth substrate (CDK) + pre-token & group-sync Lambdas
Stands up the real AWS auth broker the servers already validate against
(SH_MCP_ENV=aws), surface-agnostic. Nothing deployed yet (gated on Google
secrets); CI synthesizes the stack.
infra/ — root CDK app, stack sh-mcp-auth:
- Cognito user pool, ESSENTIALS feature plan (required for the V2 pre-token
trigger), Google external OIDC IdP (client_id/secret resolved from Secrets
Manager at deploy via CFN dynamic reference, never inlined).
- Resource servers + per-tier app clients whose AllowedOAuthScopes ARE the
trust-tier boundary: ops=(read,tasks), exec=(ops+gmail/calendar, NO finance),
finance=(finance:read ONLY, 15-min access TTL). offline refresh 30d.
- Cognito groups sh-mcp-ops/-assistant/-finance/-admin.
- sync-state + deny-list DynamoDB tables (overrideLogicalId pinned so a future
refactor cannot replace+drop them; deny-list TTL attr 'expiresAt').
- Least-priv IAM (no wildcard action/resource; Google SA secret grant scoped to
the one secret), arm64 Lambdas, explicit 60-day log groups, alarms on the
seahaven-alarm-topics CMK (ALARM-state actions only, two-alarm group-sync).
auth/pre-token-gen — SUPPRESS-ONLY V2 Lambda. Maps Cognito group entitlement to
scopesToSuppress; NEVER scopesToAdd a tier scope (AllowedOAuthScopes stays the
ceiling). Reads last_successful_sync; fail-closed to base ops:read when stale.
auth/group-sync — mirrors Google Group membership into Cognito groups every 5 min
(jose-signed SA JWT -> Directory API, no googleapis dep); writes the freshness
marker ONLY on full success so a partial failure keeps the pre-token Lambda
failing closed.
37 new tests (suppress-only policy, fail-closed, reconcile diff, 16 CDK
assertions incl. Essentials/V2/per-client-scope/no-wildcard-IAM). 448 total pass;
tsc -b + infra typecheck + cdk synth + prettier clean; CI run-cdk-synth re-enabled.
App-client callback URLs are a context placeholder pending the surface decision.
Confluence map (1540098) + project memory updates owed once this deploys.
* Phase 2a: harden auth substrate per security-review + IAM cross-review
Both mandatory gates run on the 2a diff. GPT-4.1 IAM/Lambda cross-review: the
suppress-only invariant is now an executable fail-closed guard (a future edit
that sets scopesToAdd throws → no token minted). /sh-security-review fan-out +
proof-or-kill verifier: PASS (0 confirmed critical/high). The verifier refuted
the two "high" candidates (the email-case revocation "bypass" is symmetric — the
add path uses the same lowercasing filter, so an un-removable user could never
have been added; the empty-directory purge is a non-200 throw → stale marker →
fail closed). Three confirmed findings remediated:
- C2 (deny-list was inert): the sh-mcp-deny-list table was provisioned and
documented as "hard revocation" but no code read it. The pre-token Lambda now
reads it on every mint (DENY_LIST_TABLE env + grantReadData) and strips a
deny-listed sub to NO tier scopes, ahead of the next group sync. Fail-OPEN on
a DDB read error (logs deny_list_read_failed) so a blip can't lock everyone
out — group membership + its fail-closed 30-min window stay authoritative.
- C5 (finance 30-day refresh nullified the 15-min access TTL): refresh window is
now per-tier; finance caps at 8h, ops/exec keep 30d.
- C7 (nested Google-group members silently dropped): listGroupMembers now sets
includeDerivedMembership and skips non-USER rows, honoring the documented
"nested resolved" contract instead of pushing a phantom group address.
Also corrects the sync.ts comment that overstated fail-closed as instantaneous
(it is bounded by MAX_SYNC_AGE_MS). +8 tests (deny-list unit, hard-revocation
handler path, finance refresh window, deny-list env wiring); 456 pass. tsc -b,
cdk synth, prettier, eslint all clean.
2026-06-26 14:33:46 -04:00
> **Status: BUILDING. Platform + auth substrate built; nothing deployed to AWS yet.**
> Phase 0b (monorepo + `@sh-mcp/shared` core), Phase 1 (runnable `sh-mcp-ops` /
> `sh-mcp-finance` over MCP + OpenAPI), and Phase 2a (the Cognito auth substrate
> CDK stack + pre-token/group-sync Lambdas) are on `main`. The servers run
> locally (`SH_MCP_ENV=local`); `SH_MCP_ENV=aws` is wired but not yet deployed.
> The authoritative spec is [`docs/design.md`](docs/design.md).
2026-07-06 17:44:42 -04:00
## Documentation
The canonical map of Sea Haven's AWS infrastructure lives in Confluence. This project's `sh-mcp-auth` , `sh-mcp-ops` , and `sh-mcp-finance` stacks are represented there as Mermaid subgraphs.
- **[AWS Architecture Map ](https://seahaven.atlassian.net/wiki/spaces/IT/pages/1540098 )** (Confluence, IT space, page 1540098)
2026-07-10 16:07:32 -04:00
## Infrastructure (CDK app)
All AWS infrastructure is defined as code with the **AWS CDK** (TypeScript, `aws-cdk-lib` pinned
in `package.json` ). The repo root is the CDK app: `cdk.json` is the app config and points the CDK
CLI at the app entry point.
```jsonc
// cdk.json
{
"app": "tsx infra/bin/app.ts", // entry point (run with tsx, no pre-compile)
"output": "cdk.out", // synth output dir (git-ignored)
"context": { "@aws -cdk/core:newStyleStackSynthesis": true }
}
```
- **Entry point** — `infra/bin/app.ts` constructs the `App` and instantiates the stacks. Every
stack sets an explicit kebab-case `stackName` and pins `env` to account `328440206208` /
`us-east-1` , so synth needs no AWS credentials (shared CMKs are referenced by ARN, not
`fromLookup` ).
- **Stacks** — `infra/lib/` . Currently one: `ShMcpAuthStack` → ** `sh-mcp-auth` ** (the Cognito auth
substrate, detailed below). The per-tier `sh-mcp-ops` / `sh-mcp-finance` hosting stacks join the
same app in Phase 2b. The Lambda handler source the stacks bundle lives in `auth/` (and later
`servers/` / `jobs/` ), not under `infra/` .
- **Deploy-time context** — the app reads two optional `-c` context inputs: `callbackUrls`
(per-tier OAuth callback URLs; defaults to a placeholder until the conversational surface is
chosen) and `logsKmsArn` (the `alias/seahaven-logs` CMK for the Lambda log groups; AWS-managed
encryption is used if absent).
```bash
# from the repo root
npm ci
npx cdk synth # synthesize CloudFormation into cdk.out/
npx cdk diff sh-mcp-auth # preview changes against the deployed stack
npx cdk deploy sh-mcp-auth \
-c logsKmsArn=< cmk-arn > \
-c callbackUrls='["https://…/oauth/callback"]'
```
CI runs `npx cdk synth` at the repo root on every PR (the `ci-typescript-cdk` reusable workflow with
2026-07-28 12:29:49 -04:00
`run-cdk-synth: true` ); deploys are triggered manually (Actions → deploy → Run workflow on
`main` , i.e. `workflow_dispatch` ) via the `cd-cdk` reusable workflow over the
2026-07-10 16:07:32 -04:00
per-repo `githubdeploy-sh-mcp` OIDC role. **Nothing is deployed to AWS yet** — see status above.
Phase 2a: Cognito auth substrate (CDK) + pre-token & group-sync Lambdas (#4)
* Phase 2a: Cognito auth substrate (CDK) + pre-token & group-sync Lambdas
Stands up the real AWS auth broker the servers already validate against
(SH_MCP_ENV=aws), surface-agnostic. Nothing deployed yet (gated on Google
secrets); CI synthesizes the stack.
infra/ — root CDK app, stack sh-mcp-auth:
- Cognito user pool, ESSENTIALS feature plan (required for the V2 pre-token
trigger), Google external OIDC IdP (client_id/secret resolved from Secrets
Manager at deploy via CFN dynamic reference, never inlined).
- Resource servers + per-tier app clients whose AllowedOAuthScopes ARE the
trust-tier boundary: ops=(read,tasks), exec=(ops+gmail/calendar, NO finance),
finance=(finance:read ONLY, 15-min access TTL). offline refresh 30d.
- Cognito groups sh-mcp-ops/-assistant/-finance/-admin.
- sync-state + deny-list DynamoDB tables (overrideLogicalId pinned so a future
refactor cannot replace+drop them; deny-list TTL attr 'expiresAt').
- Least-priv IAM (no wildcard action/resource; Google SA secret grant scoped to
the one secret), arm64 Lambdas, explicit 60-day log groups, alarms on the
seahaven-alarm-topics CMK (ALARM-state actions only, two-alarm group-sync).
auth/pre-token-gen — SUPPRESS-ONLY V2 Lambda. Maps Cognito group entitlement to
scopesToSuppress; NEVER scopesToAdd a tier scope (AllowedOAuthScopes stays the
ceiling). Reads last_successful_sync; fail-closed to base ops:read when stale.
auth/group-sync — mirrors Google Group membership into Cognito groups every 5 min
(jose-signed SA JWT -> Directory API, no googleapis dep); writes the freshness
marker ONLY on full success so a partial failure keeps the pre-token Lambda
failing closed.
37 new tests (suppress-only policy, fail-closed, reconcile diff, 16 CDK
assertions incl. Essentials/V2/per-client-scope/no-wildcard-IAM). 448 total pass;
tsc -b + infra typecheck + cdk synth + prettier clean; CI run-cdk-synth re-enabled.
App-client callback URLs are a context placeholder pending the surface decision.
Confluence map (1540098) + project memory updates owed once this deploys.
* Phase 2a: harden auth substrate per security-review + IAM cross-review
Both mandatory gates run on the 2a diff. GPT-4.1 IAM/Lambda cross-review: the
suppress-only invariant is now an executable fail-closed guard (a future edit
that sets scopesToAdd throws → no token minted). /sh-security-review fan-out +
proof-or-kill verifier: PASS (0 confirmed critical/high). The verifier refuted
the two "high" candidates (the email-case revocation "bypass" is symmetric — the
add path uses the same lowercasing filter, so an un-removable user could never
have been added; the empty-directory purge is a non-200 throw → stale marker →
fail closed). Three confirmed findings remediated:
- C2 (deny-list was inert): the sh-mcp-deny-list table was provisioned and
documented as "hard revocation" but no code read it. The pre-token Lambda now
reads it on every mint (DENY_LIST_TABLE env + grantReadData) and strips a
deny-listed sub to NO tier scopes, ahead of the next group sync. Fail-OPEN on
a DDB read error (logs deny_list_read_failed) so a blip can't lock everyone
out — group membership + its fail-closed 30-min window stay authoritative.
- C5 (finance 30-day refresh nullified the 15-min access TTL): refresh window is
now per-tier; finance caps at 8h, ops/exec keep 30d.
- C7 (nested Google-group members silently dropped): listGroupMembers now sets
includeDerivedMembership and skips non-USER rows, honoring the documented
"nested resolved" contract instead of pushing a phantom group address.
Also corrects the sync.ts comment that overstated fail-closed as instantaneous
(it is bounded by MAX_SYNC_AGE_MS). +8 tests (deny-list unit, hard-revocation
handler path, finance refresh window, deny-list env wiring); 456 pass. tsc -b,
cdk synth, prettier, eslint all clean.
2026-06-26 14:33:46 -04:00
## Auth substrate (Phase 2a — `infra/` + `auth/`)
`infra/` is the root CDK app; `cdk synth` builds the ** `sh-mcp-auth` ** stack:
a Google-federated Cognito user pool (ESSENTIALS feature plan), per-tier app
clients whose `AllowedOAuthScopes` are the trust-tier boundary
(`sh-agentforce-ops` / `-finance` / `-exec` ), a **suppress-only** V2 pre-token
Lambda (`auth/pre-token-gen` ), a 5-minute group-sync Lambda
(`auth/group-sync` ), and the sync-state + deny-list tables.
**Revocation has two layers.** Group membership (Google → Cognito, 5-min cadence)
is the primary entitlement control, with a fail-**closed** 30-minute freshness
window: if group-sync stalls, the pre-token Lambda drops every caller to base
`ops:read` . The `sh-mcp-deny-list` table is a hard-kill **overlay** the pre-token
Lambda consults on every mint — a `sub` listed there is stripped to no tier scopes
immediately (TTL-expiring), ahead of the next sync. The deny-list read is fail-**open**
(a DynamoDB blip logs `deny_list_read_failed` and does not lock everyone out, since
group membership + its fail-closed window remain authoritative). The finance app
client additionally caps its refresh token at 8h (vs 30d for ops/exec) so a stolen
finance refresh token cannot ride past the short access-token TTL for a month.
**Deploy is gated on two manual prerequisites (owner: Adam):**
1. Google Cloud OAuth 2.0 web client (Cognito federation) → Secrets Manager `sh-mcp/google-oidc` (`{client_id, client_secret}` ).
2. Google Workspace service account w/ domain-wide delegation (Directory `groups.readonly` ) → Secrets Manager `sh-mcp/google-directory-sa` .
**App-client OAuth callback URLs are a placeholder** (`-c callbackUrls=...` )
until the conversational surface (Agentforce vs Bolt) is chosen — the substrate
is surface-agnostic. The optional `alias/seahaven-logs` CMK for the Lambda log
groups is supplied at deploy via `-c logsKmsArn=...` .
2026-06-09 19:25:24 -04:00
## Shape (planned)
- **MCP servers** (trust-tiered, remote HTTP, per-server IAM):
- `sh-mcp-ops` — read-mostly, agent-facing (WO/PO/site lookups, KB search, Google Maps,
Gmail/Calendar, tasks, reminders).
- `sh-mcp-finance` — sensitive, read-only, audited (QBO vendor search, payment lookups).
- `sh-mcp-physical` — DEFERRED, admin/out-of-band only (Lenel/Yealink/3CX control).
- **Auth** — Google Workspace is the single IdP; an Amazon Cognito user pool federated to Google
issues scoped, audience-bound JWTs; group → scope mapping via a pre-token Lambda. See design §2.
- **Jobs** — rebuilt proactive Lambdas (email classify/digest, KB syncs).
- **Language** — TypeScript everywhere (servers, packages, CDK, jobs).
## Open decisions
- Task-agent surface: Agentforce (recommended) vs marketplace Claude app vs custom Bolt assistant
(design §12). Drives the model + guardrail story.
- Endpoint exposure specifics (Slack egress ranges / WAF) — design §9 / §11.
## Layout (target)
```
2026-07-10 16:07:32 -04:00
cdk.json CDK app config (app → infra/bin/app.ts)
infra/ CDK app: bin/app.ts entry + lib/ stack definitions
2026-06-09 19:25:24 -04:00
packages/ shared + one package per integration
servers/ sh-mcp-ops, sh-mcp-finance (CDK stacks)
auth/ cognito, pre-token-lambda, group-sync-lambda
jobs/ rebuilt scheduled Lambdas
docs/ design.md (the canonical plan)
```
See [`docs/design.md` ](docs/design.md ) for the authoritative spec.