mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-07 13:58:58 +00:00
Add runnable sh-mcp-ops and sh-mcp-finance servers
Two thin composition-root servers over the shared transport (design.md §3): - ops: internal-data, knowledge-base, google-maps, gmail, calendar, tasks, reminders. finance: qbo, payments (audited + redacted on egress). - config from env only (no hardcoded ids/issuer/tables); SH_MCP_ENV selects LocalAuthProvider + dev clients (local) vs CognitoAuthProvider + real stubs (aws). Finance applies the 15-min finance-token TTL ceiling (design.md §2.5). - index.ts is the only place .listen() is called; a Lambda handler placeholder is exported but not depended on. - synth-only CDK stubs (no real IAM/Cognito/WAF) so 'cdk synth' has a valid app (build-plan §6); READMEs document local run, dev tokens, curl, MCP Inspector.
This commit is contained in:
parent
2e16c306b0
commit
9bf85aef29
20 changed files with 940 additions and 0 deletions
81
servers/sh-mcp-finance/README.md
Normal file
81
servers/sh-mcp-finance/README.md
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
# sh-mcp-finance
|
||||||
|
|
||||||
|
Finance-tier Sea Haven MCP server. Exposes the **finance** tool registry
|
||||||
|
(qbo `search_vendors`, payments `lookup_payment_by_*` — see `docs/design.md §3`)
|
||||||
|
over the same two interfaces as `sh-mcp-ops` (MCP Streamable HTTP + OpenAPI 3.1),
|
||||||
|
through the same shared dispatch path.
|
||||||
|
|
||||||
|
Finance is **sensitive, read-only, and fully audited**: every tool call emits a
|
||||||
|
structured audit record and the response is redacted on egress (bank account /
|
||||||
|
routing / card / SSN / tax-id masked) before it leaves the server
|
||||||
|
(`docs/design.md §2.5, §7.3`).
|
||||||
|
|
||||||
|
## Run locally (no AWS, no Cognito)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
SH_MCP_ENV=local PORT=8082 npm run start -w @sh-mcp/server-finance
|
||||||
|
# or: SH_MCP_ENV=local npm run dev -w @sh-mcp/server-finance
|
||||||
|
```
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
Identical shape to `sh-mcp-ops`: `GET /healthz`, `GET /openapi.json` (both
|
||||||
|
unauthenticated), `POST /mcp`, and `POST /tools/:name` (both authenticated).
|
||||||
|
|
||||||
|
### Dev bearer tokens (local only)
|
||||||
|
|
||||||
|
| Token | Identity | Scopes |
|
||||||
|
| ------------------- | -------------------------- | ------------------------------------------- |
|
||||||
|
| `dev-finance` | accounting@seahavenind.com | `ops:read`, `finance:read` |
|
||||||
|
| `dev-finance-admin` | adam@seahavenind.com | `ops:read`, `finance:read`, `finance:admin` |
|
||||||
|
|
||||||
|
Ops-tier tokens (`dev-ops-only`, `dev-assistant`) are **rejected** by this server
|
||||||
|
(audience binding).
|
||||||
|
|
||||||
|
### Sample curl — redaction on egress
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=dev-finance
|
||||||
|
|
||||||
|
curl -s -X POST localhost:8082/tools/lookup_payment_by_vendor \
|
||||||
|
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"vendor":"Harbor"}' | jq
|
||||||
|
# → bankAccountNumber / bankRoutingNumber / cardNumber are "[REDACTED]";
|
||||||
|
# vendor, amount, invoice number are intact.
|
||||||
|
```
|
||||||
|
|
||||||
|
Each call writes one structured audit line to stdout (CloudWatch in Lambda):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": "audit",
|
||||||
|
"sub": "...",
|
||||||
|
"tool": "lookup_payment_by_vendor",
|
||||||
|
"argsHash": "<sha256>",
|
||||||
|
"decision": "allow",
|
||||||
|
"result": "ok",
|
||||||
|
"ts": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Args are **hashed, never logged raw** — secrets never reach the audit log.
|
||||||
|
|
||||||
|
### MCP Inspector
|
||||||
|
|
||||||
|
Point it at `http://localhost:8082/mcp` (Streamable HTTP) with
|
||||||
|
`Authorization: Bearer dev-finance`.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
Same as `sh-mcp-ops` plus `PAYMENTS_TABLE` (aws mode). Finance additionally
|
||||||
|
applies the **15-minute TTL ceiling** on `finance:*` tokens in `aws` mode
|
||||||
|
(`docs/design.md §2.5`). `aws` mode is not runtime-exercised in Phase 1.
|
||||||
|
|
||||||
|
## CDK
|
||||||
|
|
||||||
|
`cdk/app.ts` is a **synth-only** placeholder (no real IAM/Cognito/WAF) — the
|
||||||
|
finance least-privilege role and audit wiring land in a later phase behind the
|
||||||
|
mandatory IAM cross-review.
|
||||||
7
servers/sh-mcp-finance/cdk.json
Normal file
7
servers/sh-mcp-finance/cdk.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"app": "tsx cdk/app.ts",
|
||||||
|
"output": "cdk.out",
|
||||||
|
"context": {
|
||||||
|
"@aws-cdk/core:newStyleStackSynthesis": true
|
||||||
|
}
|
||||||
|
}
|
||||||
30
servers/sh-mcp-finance/cdk/app.ts
Normal file
30
servers/sh-mcp-finance/cdk/app.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* sh-mcp-finance — synth-only CDK app (build-plan §6).
|
||||||
|
*
|
||||||
|
* Exists ONLY so the CI `cdk synth` gate has a valid app to synthesize. Defines
|
||||||
|
* NO real IAM/Cognito/API-Gateway/WAF resources (those need the mandatory human
|
||||||
|
* IAM cross-review). The real stack — including the finance least-privilege role
|
||||||
|
* and audit wiring (design.md §2.5) — is a later phase.
|
||||||
|
*
|
||||||
|
* TODO(phase-2): real stack — gated on Cognito + IAM cross-review (design.md §8).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { App, Stack, CfnOutput, type StackProps } from 'aws-cdk-lib';
|
||||||
|
import type { Construct } from 'constructs';
|
||||||
|
|
||||||
|
class ShMcpFinanceStack extends Stack {
|
||||||
|
constructor(scope: Construct, id: string, props?: StackProps) {
|
||||||
|
super(scope, id, props);
|
||||||
|
|
||||||
|
new CfnOutput(this, 'PlatformTier', {
|
||||||
|
value: 'finance',
|
||||||
|
description: 'sh-mcp-finance trust tier (synth-only placeholder; design.md §3).',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
new ShMcpFinanceStack(app, 'sh-mcp-finance', {
|
||||||
|
description: 'Sea Haven MCP finance-tier server (synth-only stub — no real infra yet).',
|
||||||
|
});
|
||||||
|
app.synth();
|
||||||
36
servers/sh-mcp-finance/package.json
Normal file
36
servers/sh-mcp-finance/package.json
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"name": "@sh-mcp/server-finance",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Sea Haven MCP finance-tier server — MCP (Streamable HTTP) + OpenAPI 3.1 over the finance tool registry, fully audited",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"build": "tsc --project tsconfig.json",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"synth": "cdk synth >/dev/null"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sh-mcp/payments": "*",
|
||||||
|
"@sh-mcp/qbo": "*",
|
||||||
|
"@sh-mcp/shared": "*",
|
||||||
|
"express": "5.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"aws-cdk-lib": "2.260.0",
|
||||||
|
"constructs": "10.6.0",
|
||||||
|
"tsx": "4.22.4",
|
||||||
|
"typescript": "^5.5.0",
|
||||||
|
"vitest": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
57
servers/sh-mcp-finance/src/app.ts
Normal file
57
servers/sh-mcp-finance/src/app.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* sh-mcp-finance application assembly.
|
||||||
|
*
|
||||||
|
* Same shared host as ops, but the registry holds finance-tier tools — so every
|
||||||
|
* call is audited and redacted on egress by the shared dispatcher (design.md
|
||||||
|
* §2.5, §7.3). Exported separately from `index.ts` for tests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConsoleAuditLogger,
|
||||||
|
InMemoryRateLimiter,
|
||||||
|
createApp,
|
||||||
|
type DispatchDeps,
|
||||||
|
} from '@sh-mcp/shared';
|
||||||
|
import type { Express } from 'express';
|
||||||
|
|
||||||
|
import { buildFinanceRegistry } from './registry.js';
|
||||||
|
import { buildFinanceAuthProvider } from './auth.js';
|
||||||
|
import { loadFinanceConfig, type FinanceConfig } from './config.js';
|
||||||
|
|
||||||
|
export interface BuildAppResult {
|
||||||
|
app: Express;
|
||||||
|
config: FinanceConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFinanceApp(configOverride?: FinanceConfig): BuildAppResult {
|
||||||
|
const config = configOverride ?? loadFinanceConfig();
|
||||||
|
const registry = buildFinanceRegistry(config);
|
||||||
|
const authProvider = buildFinanceAuthProvider(config);
|
||||||
|
|
||||||
|
const deps: DispatchDeps = {
|
||||||
|
auditLogger: new ConsoleAuditLogger(),
|
||||||
|
// Finance is read-only and lower-volume; cap tighter than ops.
|
||||||
|
rateLimiter: new InMemoryRateLimiter({
|
||||||
|
sessionCap: 100,
|
||||||
|
perToolLimit: 30,
|
||||||
|
windowMs: 60_000,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
registry,
|
||||||
|
authProvider,
|
||||||
|
deps,
|
||||||
|
mcpInfo: { name: 'sh-mcp-finance', version: '0.1.0' },
|
||||||
|
openApi: {
|
||||||
|
info: {
|
||||||
|
title: 'Sea Haven MCP — Finance',
|
||||||
|
version: '0.1.0',
|
||||||
|
description: 'Finance-tier tools (sensitive, read-only, audited). design.md §3.',
|
||||||
|
},
|
||||||
|
servers: [{ url: `http://localhost:${config.port}`, description: 'local' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { app, config };
|
||||||
|
}
|
||||||
31
servers/sh-mcp-finance/src/auth.ts
Normal file
31
servers/sh-mcp-finance/src/auth.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* sh-mcp-finance AuthProvider selection.
|
||||||
|
*
|
||||||
|
* `aws` → CognitoAuthProvider with the finance TTL ceiling + finance scope
|
||||||
|
* prefix (design.md §2.5).
|
||||||
|
* `local` → LocalAuthProvider (dev bearer tokens; refuses to build unless
|
||||||
|
* SH_MCP_ENV=local).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
CognitoAuthProvider,
|
||||||
|
LocalAuthProvider,
|
||||||
|
defaultLocalPrincipals,
|
||||||
|
type AuthProvider,
|
||||||
|
} from '@sh-mcp/shared';
|
||||||
|
|
||||||
|
import type { FinanceConfig } from './config.js';
|
||||||
|
|
||||||
|
export function buildFinanceAuthProvider(config: FinanceConfig): AuthProvider {
|
||||||
|
if (config.env === 'aws') {
|
||||||
|
if (!config.cognito) {
|
||||||
|
throw new Error('Cognito config missing in aws mode.');
|
||||||
|
}
|
||||||
|
return new CognitoAuthProvider(config.cognito);
|
||||||
|
}
|
||||||
|
return new LocalAuthProvider({
|
||||||
|
audience: config.audience,
|
||||||
|
principals: defaultLocalPrincipals(),
|
||||||
|
env: 'local',
|
||||||
|
});
|
||||||
|
}
|
||||||
66
servers/sh-mcp-finance/src/config.ts
Normal file
66
servers/sh-mcp-finance/src/config.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* sh-mcp-finance configuration — read entirely from the environment.
|
||||||
|
*
|
||||||
|
* Like ops, nothing is hardcoded (build-plan §7). Finance additionally applies
|
||||||
|
* the 15-minute TTL ceiling on finance-scoped tokens (design.md §2.5).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { cognitoIssuer, cognitoJwks, type CognitoAuthConfig } from '@sh-mcp/shared';
|
||||||
|
|
||||||
|
export type Env = 'local' | 'aws';
|
||||||
|
|
||||||
|
export interface FinanceConfig {
|
||||||
|
env: Env;
|
||||||
|
port: number;
|
||||||
|
audience: string;
|
||||||
|
cognito?: CognitoAuthConfig;
|
||||||
|
paymentsTable?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUDIENCE = 'sh-mcp-finance';
|
||||||
|
const SCOPE_PREFIX = 'sh-mcp-finance';
|
||||||
|
/** design.md §2.5: finance:* tokens must be short-lived (≤ 15 min). */
|
||||||
|
const FINANCE_TTL_SECONDS = 15 * 60;
|
||||||
|
|
||||||
|
function readEnv(name: string): string | undefined {
|
||||||
|
const v = process.env[name];
|
||||||
|
return v !== undefined && v.length > 0 ? v : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadFinanceConfig(): FinanceConfig {
|
||||||
|
const rawEnv = readEnv('SH_MCP_ENV') ?? 'local';
|
||||||
|
if (rawEnv !== 'local' && rawEnv !== 'aws') {
|
||||||
|
throw new Error(`SH_MCP_ENV must be "local" or "aws" (got "${rawEnv}").`);
|
||||||
|
}
|
||||||
|
const env = rawEnv;
|
||||||
|
const port = Number(readEnv('PORT') ?? '8082');
|
||||||
|
|
||||||
|
const base: FinanceConfig = { env, port, audience: AUDIENCE };
|
||||||
|
|
||||||
|
if (env === 'aws') {
|
||||||
|
const region = readEnv('AWS_REGION') ?? 'us-east-1';
|
||||||
|
const userPoolId = required('COGNITO_USER_POOL_ID');
|
||||||
|
const allowedClientIds = required('COGNITO_ALLOWED_CLIENT_IDS')
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
base.cognito = {
|
||||||
|
issuer: cognitoIssuer(region, userPoolId),
|
||||||
|
audience: AUDIENCE,
|
||||||
|
allowedClientIds,
|
||||||
|
scopePrefix: SCOPE_PREFIX,
|
||||||
|
jwks: cognitoJwks(region, userPoolId),
|
||||||
|
maxTtlSeconds: FINANCE_TTL_SECONDS,
|
||||||
|
ttlGuardedScopes: ['finance:read', 'finance:admin'],
|
||||||
|
};
|
||||||
|
base.paymentsTable = readEnv('PAYMENTS_TABLE');
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
function required(name: string): string {
|
||||||
|
const v = readEnv(name);
|
||||||
|
if (!v) throw new Error(`Missing required env var ${name} for SH_MCP_ENV=aws.`);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
39
servers/sh-mcp-finance/src/index.ts
Normal file
39
servers/sh-mcp-finance/src/index.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* sh-mcp-finance entrypoint. The only place a listener is created
|
||||||
|
* (build-plan §2.1). A Lambda `handler` placeholder is exported for a future
|
||||||
|
* phase but is not depended on here.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { buildFinanceApp } from './app.js';
|
||||||
|
|
||||||
|
export { buildFinanceApp } from './app.js';
|
||||||
|
|
||||||
|
function main(): void {
|
||||||
|
const { app, config } = buildFinanceApp();
|
||||||
|
app.listen(config.port, () => {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
msg: 'sh-mcp-finance listening',
|
||||||
|
env: config.env,
|
||||||
|
port: config.port,
|
||||||
|
endpoints: ['/mcp', '/openapi.json', 'POST /tools/:name', '/healthz'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Placeholder Lambda handler for a future phase — intentionally throws. */
|
||||||
|
export function handler(): never {
|
||||||
|
throw new Error('Lambda handler is not implemented in Phase 1; run the HTTP server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const invokedDirectly =
|
||||||
|
process.argv[1] !== undefined && import.meta.url === `file://${process.argv[1]}`;
|
||||||
|
if (invokedDirectly) {
|
||||||
|
try {
|
||||||
|
main();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to start sh-mcp-finance:', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
servers/sh-mcp-finance/src/registry.ts
Normal file
46
servers/sh-mcp-finance/src/registry.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* sh-mcp-finance tool registry — composition root.
|
||||||
|
*
|
||||||
|
* Registers exactly the finance-tier tools (design.md §3): qbo (search_vendors)
|
||||||
|
* and payments (lookup_payment_by_*). Every finance tool call is audited and
|
||||||
|
* redacted on egress by the shared dispatcher (design.md §2.5, §7.3).
|
||||||
|
*
|
||||||
|
* As with ops, we call each factory with an explicit client rather than
|
||||||
|
* importing a pre-wired `tools` array, to keep import-time side-effect free
|
||||||
|
* (build-plan §7).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ToolRegistry, type ToolDef } from '@sh-mcp/shared';
|
||||||
|
|
||||||
|
import {
|
||||||
|
makeSearchVendorsTool,
|
||||||
|
QboClientImpl,
|
||||||
|
type QboClientInterface,
|
||||||
|
InMemoryQboClient,
|
||||||
|
} from '@sh-mcp/qbo';
|
||||||
|
import {
|
||||||
|
makePaymentsTools,
|
||||||
|
DynamoPaymentsClient,
|
||||||
|
type PaymentsClient,
|
||||||
|
InMemoryPaymentsClient,
|
||||||
|
} from '@sh-mcp/payments';
|
||||||
|
|
||||||
|
import type { FinanceConfig } from './config.js';
|
||||||
|
|
||||||
|
/** Build the finance registry for the given environment. */
|
||||||
|
export function buildFinanceRegistry(config: FinanceConfig): ToolRegistry {
|
||||||
|
const local = config.env === 'local';
|
||||||
|
|
||||||
|
const qboClient: QboClientInterface = local ? new InMemoryQboClient() : new QboClientImpl();
|
||||||
|
const paymentsClient: PaymentsClient = local
|
||||||
|
? new InMemoryPaymentsClient()
|
||||||
|
: new DynamoPaymentsClient(config.paymentsTable);
|
||||||
|
|
||||||
|
const registry = new ToolRegistry();
|
||||||
|
const all: ToolDef<unknown, unknown>[] = [
|
||||||
|
makeSearchVendorsTool(qboClient),
|
||||||
|
...makePaymentsTools(paymentsClient),
|
||||||
|
].map((t) => t as ToolDef<unknown, unknown>);
|
||||||
|
for (const tool of all) registry.register(tool);
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
14
servers/sh-mcp-finance/tsconfig.json
Normal file
14
servers/sh-mcp-finance/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"declarationDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../../packages/shared" },
|
||||||
|
{ "path": "../../packages/payments" },
|
||||||
|
{ "path": "../../packages/qbo" }
|
||||||
|
]
|
||||||
|
}
|
||||||
97
servers/sh-mcp-ops/README.md
Normal file
97
servers/sh-mcp-ops/README.md
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
# sh-mcp-ops
|
||||||
|
|
||||||
|
Operations-tier Sea Haven MCP server. Exposes the **ops** tool registry
|
||||||
|
(internal-data, knowledge-base, google-maps, gmail, calendar, tasks, reminders —
|
||||||
|
see `docs/design.md §3`) over **two universal interfaces backed by one shared
|
||||||
|
dispatch path**:
|
||||||
|
|
||||||
|
- **MCP** (Streamable HTTP) at `POST /mcp` — via `@modelcontextprotocol/sdk`.
|
||||||
|
- **OpenAPI 3.1** — full document at `GET /openapi.json`, plus one
|
||||||
|
`POST /tools/{tool-name}` endpoint per tool.
|
||||||
|
|
||||||
|
All scope enforcement, audience binding, input validation, rate limiting, and
|
||||||
|
audit logging live in `@sh-mcp/shared` (`executeTool`) and are identical across
|
||||||
|
both interfaces (`docs/design.md §2.5`).
|
||||||
|
|
||||||
|
## Run locally (no AWS, no Cognito)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# from the repo root
|
||||||
|
npm install
|
||||||
|
npm run build # or: npx tsc -b
|
||||||
|
|
||||||
|
SH_MCP_ENV=local PORT=8081 npm run start -w @sh-mcp/server-ops
|
||||||
|
# or live-reload: SH_MCP_ENV=local npm run dev -w @sh-mcp/server-ops
|
||||||
|
```
|
||||||
|
|
||||||
|
`SH_MCP_ENV=local` wires **in-memory dev clients** (seeded fake data, no network)
|
||||||
|
and the `LocalAuthProvider`, which maps dev bearer tokens to identities. The
|
||||||
|
local provider refuses to construct unless `SH_MCP_ENV=local`.
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Auth | Notes |
|
||||||
|
| ------ | -------------------- | ---- | -------------------------------------- |
|
||||||
|
| GET | `/healthz` | no | `{ "status": "ok" }` |
|
||||||
|
| GET | `/openapi.json` | no | full OpenAPI 3.1 document |
|
||||||
|
| POST | `/mcp` (+GET/DELETE) | yes | MCP Streamable HTTP |
|
||||||
|
| POST | `/tools/:name` | yes | one-shot tool call, JSON in / JSON out |
|
||||||
|
|
||||||
|
### Dev bearer tokens (local only)
|
||||||
|
|
||||||
|
| Token | Identity | Scopes |
|
||||||
|
| --------------- | ------------------------ | -------------------------------------------- |
|
||||||
|
| `dev-ops-only` | ops-only@seahavenind.com | `ops:read`, `ops:tasks` |
|
||||||
|
| `dev-assistant` | lauren@seahavenind.com | + `gmail:self`, `calendar:self` |
|
||||||
|
| `dev-ops-admin` | adam@seahavenind.com | `ops:read`, `ops:tasks`, gmail/calendar self |
|
||||||
|
|
||||||
|
Tokens minted for a different tier (e.g. `dev-finance`) are **rejected** by this
|
||||||
|
server (audience binding).
|
||||||
|
|
||||||
|
### Sample curl
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=dev-ops-only
|
||||||
|
|
||||||
|
curl -s localhost:8081/healthz
|
||||||
|
curl -s localhost:8081/openapi.json | jq '.openapi, (.paths | keys | length)'
|
||||||
|
|
||||||
|
curl -s -X POST localhost:8081/tools/lookup_work_order \
|
||||||
|
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"workOrderId":"WO-1001"}' | jq
|
||||||
|
|
||||||
|
# tool-hiding is convenience; the boundary is server-side — a forced call to a
|
||||||
|
# tool you lack scope for is still 403:
|
||||||
|
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8081/tools/search_inbox \
|
||||||
|
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"query":"invoice"}' # → 403 (needs gmail:self)
|
||||||
|
```
|
||||||
|
|
||||||
|
### MCP Inspector
|
||||||
|
|
||||||
|
Point the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at
|
||||||
|
`http://localhost:8081/mcp` (transport: **Streamable HTTP**) and set an
|
||||||
|
`Authorization: Bearer dev-assistant` header. `tools/list` reflects only the
|
||||||
|
tools your token's scopes permit.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
| Var | Mode | Purpose |
|
||||||
|
| ---------------------------- | ---- | --------------------------------------------- |
|
||||||
|
| `SH_MCP_ENV` | both | `local` (dev clients) or `aws` (real/Cognito) |
|
||||||
|
| `PORT` | both | listen port (default 8081) |
|
||||||
|
| `AWS_REGION` | aws | Cognito region |
|
||||||
|
| `COGNITO_USER_POOL_ID` | aws | issuer / JWKS source |
|
||||||
|
| `COGNITO_ALLOWED_CLIENT_IDS` | aws | comma-list; the audience boundary |
|
||||||
|
| `GOOGLE_MAPS_API_KEY` | aws | maps client |
|
||||||
|
| `REMINDER_TARGET_ARN` etc. | aws | scheduler wiring |
|
||||||
|
|
||||||
|
`aws` mode is not runtime-exercised in Phase 1 (real external clients are
|
||||||
|
deferred stubs; Cognito infra is out of scope — see `docs/build-plan-phase-1.md`).
|
||||||
|
|
||||||
|
## CDK
|
||||||
|
|
||||||
|
`cdk/app.ts` is a **synth-only** placeholder (`npm run synth -w @sh-mcp/server-ops`)
|
||||||
|
so the CI `cdk synth` gate has a valid app. It provisions **no** real IAM,
|
||||||
|
Cognito, API Gateway, or WAF — those land in a later phase behind the mandatory
|
||||||
|
IAM cross-review.
|
||||||
7
servers/sh-mcp-ops/cdk.json
Normal file
7
servers/sh-mcp-ops/cdk.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"app": "tsx cdk/app.ts",
|
||||||
|
"output": "cdk.out",
|
||||||
|
"context": {
|
||||||
|
"@aws-cdk/core:newStyleStackSynthesis": true
|
||||||
|
}
|
||||||
|
}
|
||||||
31
servers/sh-mcp-ops/cdk/app.ts
Normal file
31
servers/sh-mcp-ops/cdk/app.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* sh-mcp-ops — synth-only CDK app (build-plan §6).
|
||||||
|
*
|
||||||
|
* Exists ONLY so the CI `cdk synth` gate has a valid app to synthesize, keeping
|
||||||
|
* the IaC/ARM64 wiring honest WITHOUT deploying. It defines NO real IAM roles,
|
||||||
|
* Cognito resources, API Gateway authorizers, or WAF — those carry the mandatory
|
||||||
|
* human IAM cross-review that cannot run here. The real stack is a later phase.
|
||||||
|
*
|
||||||
|
* TODO(phase-2): real stack — gated on Cognito + IAM cross-review (design.md §8).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { App, Stack, CfnOutput, type StackProps } from 'aws-cdk-lib';
|
||||||
|
import type { Construct } from 'constructs';
|
||||||
|
|
||||||
|
class ShMcpOpsStack extends Stack {
|
||||||
|
constructor(scope: Construct, id: string, props?: StackProps) {
|
||||||
|
super(scope, id, props);
|
||||||
|
|
||||||
|
// Inert marker output only — no real resources are provisioned here.
|
||||||
|
new CfnOutput(this, 'PlatformTier', {
|
||||||
|
value: 'ops',
|
||||||
|
description: 'sh-mcp-ops trust tier (synth-only placeholder; design.md §3).',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
new ShMcpOpsStack(app, 'sh-mcp-ops', {
|
||||||
|
description: 'Sea Haven MCP ops-tier server (synth-only stub — no real infra yet).',
|
||||||
|
});
|
||||||
|
app.synth();
|
||||||
41
servers/sh-mcp-ops/package.json
Normal file
41
servers/sh-mcp-ops/package.json
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"name": "@sh-mcp/server-ops",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Sea Haven MCP ops-tier server — MCP (Streamable HTTP) + OpenAPI 3.1 over the ops tool registry",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"build": "tsc --project tsconfig.json",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"synth": "cdk synth >/dev/null"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sh-mcp/calendar": "*",
|
||||||
|
"@sh-mcp/gmail": "*",
|
||||||
|
"@sh-mcp/google-maps": "*",
|
||||||
|
"@sh-mcp/internal-data": "*",
|
||||||
|
"@sh-mcp/knowledge-base": "*",
|
||||||
|
"@sh-mcp/reminders": "*",
|
||||||
|
"@sh-mcp/shared": "*",
|
||||||
|
"@sh-mcp/tasks": "*",
|
||||||
|
"express": "5.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"aws-cdk-lib": "2.260.0",
|
||||||
|
"constructs": "10.6.0",
|
||||||
|
"tsx": "4.22.4",
|
||||||
|
"typescript": "^5.5.0",
|
||||||
|
"vitest": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
58
servers/sh-mcp-ops/src/app.ts
Normal file
58
servers/sh-mcp-ops/src/app.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* sh-mcp-ops application assembly.
|
||||||
|
*
|
||||||
|
* Wires the registry, auth provider, audit logger, and rate limiter into the
|
||||||
|
* shared Express host (build-plan §2.2). Exported separately from `index.ts` so
|
||||||
|
* tests can build the app without binding a port.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConsoleAuditLogger,
|
||||||
|
InMemoryRateLimiter,
|
||||||
|
createApp,
|
||||||
|
type DispatchDeps,
|
||||||
|
} from '@sh-mcp/shared';
|
||||||
|
import type { Express } from 'express';
|
||||||
|
|
||||||
|
import { buildOpsRegistry } from './registry.js';
|
||||||
|
import { buildOpsAuthProvider } from './auth.js';
|
||||||
|
import { loadOpsConfig, type OpsConfig } from './config.js';
|
||||||
|
|
||||||
|
export interface BuildAppResult {
|
||||||
|
app: Express;
|
||||||
|
config: OpsConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build the ops server app. Pass a config to override env loading (tests). */
|
||||||
|
export async function buildOpsApp(configOverride?: OpsConfig): Promise<BuildAppResult> {
|
||||||
|
const config = configOverride ?? loadOpsConfig();
|
||||||
|
const registry = await buildOpsRegistry(config);
|
||||||
|
const authProvider = buildOpsAuthProvider(config);
|
||||||
|
|
||||||
|
const deps: DispatchDeps = {
|
||||||
|
auditLogger: new ConsoleAuditLogger(),
|
||||||
|
// Generous local limits; tightened per-tier in production (design.md §7.3).
|
||||||
|
rateLimiter: new InMemoryRateLimiter({
|
||||||
|
sessionCap: 200,
|
||||||
|
perToolLimit: 60,
|
||||||
|
windowMs: 60_000,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
registry,
|
||||||
|
authProvider,
|
||||||
|
deps,
|
||||||
|
mcpInfo: { name: 'sh-mcp-ops', version: '0.1.0' },
|
||||||
|
openApi: {
|
||||||
|
info: {
|
||||||
|
title: 'Sea Haven MCP — Ops',
|
||||||
|
version: '0.1.0',
|
||||||
|
description: 'Operations-tier tools (read-mostly). design.md §3.',
|
||||||
|
},
|
||||||
|
servers: [{ url: `http://localhost:${config.port}`, description: 'local' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { app, config };
|
||||||
|
}
|
||||||
30
servers/sh-mcp-ops/src/auth.ts
Normal file
30
servers/sh-mcp-ops/src/auth.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* sh-mcp-ops AuthProvider selection.
|
||||||
|
*
|
||||||
|
* `aws` → CognitoAuthProvider (real JWT verification, design.md §2.5).
|
||||||
|
* `local` → LocalAuthProvider (dev bearer tokens; refuses to build unless
|
||||||
|
* SH_MCP_ENV=local, build-plan §3).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
CognitoAuthProvider,
|
||||||
|
LocalAuthProvider,
|
||||||
|
defaultLocalPrincipals,
|
||||||
|
type AuthProvider,
|
||||||
|
} from '@sh-mcp/shared';
|
||||||
|
|
||||||
|
import type { OpsConfig } from './config.js';
|
||||||
|
|
||||||
|
export function buildOpsAuthProvider(config: OpsConfig): AuthProvider {
|
||||||
|
if (config.env === 'aws') {
|
||||||
|
if (!config.cognito) {
|
||||||
|
throw new Error('Cognito config missing in aws mode.');
|
||||||
|
}
|
||||||
|
return new CognitoAuthProvider(config.cognito);
|
||||||
|
}
|
||||||
|
return new LocalAuthProvider({
|
||||||
|
audience: config.audience,
|
||||||
|
principals: defaultLocalPrincipals(),
|
||||||
|
env: 'local',
|
||||||
|
});
|
||||||
|
}
|
||||||
70
servers/sh-mcp-ops/src/config.ts
Normal file
70
servers/sh-mcp-ops/src/config.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
* sh-mcp-ops configuration — read entirely from the environment.
|
||||||
|
*
|
||||||
|
* Nothing is hardcoded (build-plan §7, design.md §2): client ids, issuer, JWKS
|
||||||
|
* URL, table names, scope prefix all arrive via env. `SH_MCP_ENV` selects local
|
||||||
|
* (dev clients + LocalAuthProvider) vs aws (real stub clients + Cognito).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { cognitoIssuer, cognitoJwks, type CognitoAuthConfig } from '@sh-mcp/shared';
|
||||||
|
|
||||||
|
export type Env = 'local' | 'aws';
|
||||||
|
|
||||||
|
export interface OpsConfig {
|
||||||
|
env: Env;
|
||||||
|
port: number;
|
||||||
|
audience: string;
|
||||||
|
/** Cognito config — only required/used in `aws` mode. */
|
||||||
|
cognito?: CognitoAuthConfig;
|
||||||
|
// Real-client settings (aws mode only; unused locally).
|
||||||
|
googleMapsApiKey?: string;
|
||||||
|
reminderTargetArn?: string;
|
||||||
|
schedulerRoleArn?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUDIENCE = 'sh-mcp-ops';
|
||||||
|
const SCOPE_PREFIX = 'sh-mcp-ops';
|
||||||
|
|
||||||
|
function readEnv(name: string): string | undefined {
|
||||||
|
const v = process.env[name];
|
||||||
|
return v !== undefined && v.length > 0 ? v : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse and validate the process environment into an {@link OpsConfig}. */
|
||||||
|
export function loadOpsConfig(): OpsConfig {
|
||||||
|
const rawEnv = readEnv('SH_MCP_ENV') ?? 'local';
|
||||||
|
if (rawEnv !== 'local' && rawEnv !== 'aws') {
|
||||||
|
throw new Error(`SH_MCP_ENV must be "local" or "aws" (got "${rawEnv}").`);
|
||||||
|
}
|
||||||
|
const env = rawEnv;
|
||||||
|
const port = Number(readEnv('PORT') ?? '8081');
|
||||||
|
|
||||||
|
const base: OpsConfig = { env, port, audience: AUDIENCE };
|
||||||
|
|
||||||
|
if (env === 'aws') {
|
||||||
|
const region = readEnv('AWS_REGION') ?? 'us-east-1';
|
||||||
|
const userPoolId = required('COGNITO_USER_POOL_ID');
|
||||||
|
const allowedClientIds = required('COGNITO_ALLOWED_CLIENT_IDS')
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
base.cognito = {
|
||||||
|
issuer: cognitoIssuer(region, userPoolId),
|
||||||
|
audience: AUDIENCE,
|
||||||
|
allowedClientIds,
|
||||||
|
scopePrefix: SCOPE_PREFIX,
|
||||||
|
jwks: cognitoJwks(region, userPoolId),
|
||||||
|
};
|
||||||
|
base.googleMapsApiKey = readEnv('GOOGLE_MAPS_API_KEY');
|
||||||
|
base.reminderTargetArn = readEnv('REMINDER_TARGET_ARN');
|
||||||
|
base.schedulerRoleArn = readEnv('SCHEDULER_ROLE_ARN');
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
function required(name: string): string {
|
||||||
|
const v = readEnv(name);
|
||||||
|
if (!v) throw new Error(`Missing required env var ${name} for SH_MCP_ENV=aws.`);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
45
servers/sh-mcp-ops/src/index.ts
Normal file
45
servers/sh-mcp-ops/src/index.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* sh-mcp-ops entrypoint.
|
||||||
|
*
|
||||||
|
* Builds the app and starts listening. This is the ONLY place a listener is
|
||||||
|
* created — importing any module above is side-effect free (build-plan §2.1).
|
||||||
|
*
|
||||||
|
* A `handler` placeholder is exported for a future Lambda adapter, but this PR
|
||||||
|
* does NOT depend on the AWS Lambda runtime (build-plan §2.2).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { buildOpsApp } from './app.js';
|
||||||
|
|
||||||
|
export { buildOpsApp } from './app.js';
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const { app, config } = await buildOpsApp();
|
||||||
|
app.listen(config.port, () => {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
msg: 'sh-mcp-ops listening',
|
||||||
|
env: config.env,
|
||||||
|
port: config.port,
|
||||||
|
endpoints: ['/mcp', '/openapi.json', 'POST /tools/:name', '/healthz'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder Lambda handler shape for a future phase. Intentionally throws —
|
||||||
|
* the HTTP server (`main`) is the supported runtime in Phase 1.
|
||||||
|
*/
|
||||||
|
export function handler(): never {
|
||||||
|
throw new Error('Lambda handler is not implemented in Phase 1; run the HTTP server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start only when executed directly (not when imported by tests).
|
||||||
|
const invokedDirectly =
|
||||||
|
process.argv[1] !== undefined && import.meta.url === `file://${process.argv[1]}`;
|
||||||
|
if (invokedDirectly) {
|
||||||
|
main().catch((err: unknown) => {
|
||||||
|
console.error('Failed to start sh-mcp-ops:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
135
servers/sh-mcp-ops/src/registry.ts
Normal file
135
servers/sh-mcp-ops/src/registry.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
/**
|
||||||
|
* sh-mcp-ops tool registry — composition root.
|
||||||
|
*
|
||||||
|
* Registers exactly the ops-tier tools (design.md §3): internal-data,
|
||||||
|
* knowledge-base, google-maps, gmail, calendar, tasks, reminders. Each package
|
||||||
|
* factory is wired with the selected client — an in-memory dev client in
|
||||||
|
* `SH_MCP_ENV=local`, or the real (stub) client in `aws` mode.
|
||||||
|
*
|
||||||
|
* IMPORTANT: we deliberately call each package's *factory* with an explicit
|
||||||
|
* client rather than importing its pre-wired `tools` array, because some of
|
||||||
|
* those arrays construct AWS clients at import time. Building clients here keeps
|
||||||
|
* import-time side-effect-free (build-plan §7 "No I/O at import time").
|
||||||
|
*
|
||||||
|
* The package factory signatures are intentionally inconsistent (build-plan §1);
|
||||||
|
* each is wired to its own shape below.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ToolRegistry, type ToolDef } from '@sh-mcp/shared';
|
||||||
|
|
||||||
|
// internal-data
|
||||||
|
import {
|
||||||
|
makeTools as makeInternalDataTools,
|
||||||
|
RealDynamoClient,
|
||||||
|
type InternalDataClient,
|
||||||
|
InMemoryInternalDataClient,
|
||||||
|
} from '@sh-mcp/internal-data';
|
||||||
|
// knowledge-base
|
||||||
|
import {
|
||||||
|
createKnowledgeBaseTools,
|
||||||
|
createBedrockClientFromEnv,
|
||||||
|
type KnowledgeBaseClient,
|
||||||
|
InMemoryKnowledgeBaseClient,
|
||||||
|
} from '@sh-mcp/knowledge-base';
|
||||||
|
// google-maps
|
||||||
|
import {
|
||||||
|
makeTools as makeMapsTools,
|
||||||
|
GooglePlacesClient,
|
||||||
|
type GoogleMapsClient,
|
||||||
|
InMemoryGoogleMapsClient,
|
||||||
|
} from '@sh-mcp/google-maps';
|
||||||
|
// gmail
|
||||||
|
import {
|
||||||
|
makeGmailTools,
|
||||||
|
GmailApiClient,
|
||||||
|
type GmailClient,
|
||||||
|
type GoogleTokenProvider as GmailTokenProvider,
|
||||||
|
InMemoryGmailClient,
|
||||||
|
} from '@sh-mcp/gmail';
|
||||||
|
// calendar
|
||||||
|
import {
|
||||||
|
buildCalendarTools,
|
||||||
|
GoogleCalendarClient,
|
||||||
|
type CalendarClient,
|
||||||
|
type GoogleTokenProvider as CalendarTokenProvider,
|
||||||
|
InMemoryCalendarClient,
|
||||||
|
} from '@sh-mcp/calendar';
|
||||||
|
// tasks
|
||||||
|
import {
|
||||||
|
buildTaskTools,
|
||||||
|
DynamoDBTasksClient,
|
||||||
|
type TasksClient,
|
||||||
|
InMemoryTasksClient,
|
||||||
|
} from '@sh-mcp/tasks';
|
||||||
|
// reminders
|
||||||
|
import {
|
||||||
|
buildReminderTools,
|
||||||
|
AwsSchedulerClient,
|
||||||
|
type SchedulerClient,
|
||||||
|
InMemorySchedulerClient,
|
||||||
|
} from '@sh-mcp/reminders';
|
||||||
|
|
||||||
|
import type { OpsConfig } from './config.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Google per-user token provider stub for `aws` mode. The REAL provider
|
||||||
|
* (per-user OAuth refresh tokens, design.md §2.4) is a later phase; in this PR
|
||||||
|
* `aws` mode is not runtime-exercised, so this throws if actually used.
|
||||||
|
*/
|
||||||
|
class StubGoogleTokenProvider implements GmailTokenProvider, CalendarTokenProvider {
|
||||||
|
async getAccessToken(): Promise<string> {
|
||||||
|
throw new Error(
|
||||||
|
'Real per-user Google token provider is not implemented in Phase 1 ' +
|
||||||
|
'(design.md §2.4 — deferred). Run with SH_MCP_ENV=local for a working server.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build the ops registry for the given environment. */
|
||||||
|
export async function buildOpsRegistry(config: OpsConfig): Promise<ToolRegistry> {
|
||||||
|
const local = config.env === 'local';
|
||||||
|
|
||||||
|
// --- select clients (dev vs real) ---
|
||||||
|
const internalDataClient: InternalDataClient = local
|
||||||
|
? new InMemoryInternalDataClient()
|
||||||
|
: await RealDynamoClient.create();
|
||||||
|
|
||||||
|
const kbClient: KnowledgeBaseClient = local
|
||||||
|
? new InMemoryKnowledgeBaseClient()
|
||||||
|
: createBedrockClientFromEnv();
|
||||||
|
|
||||||
|
const mapsClient: GoogleMapsClient = local
|
||||||
|
? new InMemoryGoogleMapsClient()
|
||||||
|
: new GooglePlacesClient(config.googleMapsApiKey ?? '');
|
||||||
|
|
||||||
|
const tokenProvider = new StubGoogleTokenProvider();
|
||||||
|
const gmailClient: GmailClient = local
|
||||||
|
? new InMemoryGmailClient()
|
||||||
|
: new GmailApiClient(tokenProvider);
|
||||||
|
const calendarClient: CalendarClient = local
|
||||||
|
? new InMemoryCalendarClient()
|
||||||
|
: new GoogleCalendarClient(tokenProvider);
|
||||||
|
|
||||||
|
const tasksClient: TasksClient = local ? new InMemoryTasksClient() : new DynamoDBTasksClient();
|
||||||
|
|
||||||
|
const schedulerClient: SchedulerClient = local
|
||||||
|
? new InMemorySchedulerClient()
|
||||||
|
: new AwsSchedulerClient({
|
||||||
|
targetArn: config.reminderTargetArn ?? '',
|
||||||
|
roleArn: config.schedulerRoleArn ?? '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- register all ops-tier tools ---
|
||||||
|
const registry = new ToolRegistry();
|
||||||
|
const all: ToolDef<unknown, unknown>[] = [
|
||||||
|
...makeInternalDataTools(internalDataClient),
|
||||||
|
...createKnowledgeBaseTools(kbClient),
|
||||||
|
...makeMapsTools(mapsClient),
|
||||||
|
...makeGmailTools(gmailClient),
|
||||||
|
...buildCalendarTools(calendarClient),
|
||||||
|
...buildTaskTools(tasksClient),
|
||||||
|
...buildReminderTools({ client: schedulerClient }),
|
||||||
|
].map((t) => t as ToolDef<unknown, unknown>);
|
||||||
|
for (const tool of all) registry.register(tool);
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
19
servers/sh-mcp-ops/tsconfig.json
Normal file
19
servers/sh-mcp-ops/tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"declarationDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../../packages/shared" },
|
||||||
|
{ "path": "../../packages/calendar" },
|
||||||
|
{ "path": "../../packages/gmail" },
|
||||||
|
{ "path": "../../packages/google-maps" },
|
||||||
|
{ "path": "../../packages/internal-data" },
|
||||||
|
{ "path": "../../packages/knowledge-base" },
|
||||||
|
{ "path": "../../packages/reminders" },
|
||||||
|
{ "path": "../../packages/tasks" }
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue