sh-mcp/packages/internal-data/src/tools.ts
Adam Moussa 8d3de8a447 Phase 0b slice: monorepo scaffold + shared core + integration packages
The 0a-INDEPENDENT code slice (one-shot via af-0b-package-slice workflow: Haiku
scaffold + Sonnet packages, Sonnet fix-to-green). Nothing deploys; no CDK/servers.

- Monorepo scaffold: npm workspaces, strict TS (NodeNext), vitest (80% gate),
  eslint 9 flat config, prettier; ci.yaml/deploy.yaml callers (Node 24, enable-qemu).
- @sh-mcp/shared: transport-agnostic core — Scope/AuthContext/ToolDef, ToolRegistry,
  redact()+maskValue() (PII), OpenAPI 3.1 generator. AUTH STUBBED behind an AuthProvider
  interface (TODO auth-layer-0a); JWT/aud/client_id/JWKS/deny-list deferred per design.md §2.
- 9 integration packages (qbo, google-maps, internal-data, payments, knowledge-base,
  gmail, calendar, tasks, reminders): tools against shared, external deps mocked behind
  injected client interfaces; finance handlers call redact().

Verified green: tsc -b clean, vitest 245/245, eslint 0 errors. Auth mechanism intentionally
deferred until the 0a spike resolves it (G16/§0.4).
2026-06-11 14:36:28 -04:00

228 lines
6.4 KiB
TypeScript

/**
* Tool definitions for the internal-data package.
*
* Three read-only ops-tier tools backed by the injected InternalDataClient:
* - lookup_work_order
* - lookup_purchase_order
* - lookup_site
*
* All three require the `ops:read` scope (§3 of design.md).
*
* Finance-tier note: these tools are ops-tier, so `redact()` is not called on
* their responses. If a future refactor moves payment or financial amounts here,
* call redact() on those fields per the design requirement that finance handlers
* MUST redact sensitive output.
*
* The client is passed in at server startup (not imported from a module-level
* singleton) so that tests can inject a mock without AWS credentials.
*/
import { defineTool, requireScope } from '@sh-mcp/shared';
import type { AuthContext } from '@sh-mcp/shared';
import type { InternalDataClient } from './client.js';
// ---------------------------------------------------------------------------
// Input / output types
// ---------------------------------------------------------------------------
export interface LookupWorkOrderInput {
workOrderId: string;
}
export interface LookupWorkOrderOutput {
found: boolean;
workOrder?: {
workOrderId: string;
title: string;
status: string;
siteId?: string;
assignedTo?: string;
createdAt: string;
updatedAt: string;
description?: string;
};
}
export interface LookupPurchaseOrderInput {
purchaseOrderId: string;
}
export interface LookupPurchaseOrderOutput {
found: boolean;
purchaseOrder?: {
purchaseOrderId: string;
vendor: string;
status: string;
totalAmount?: number;
currency?: string;
issuedAt: string;
updatedAt: string;
lineItems?: Array<{ description: string; quantity: number; unitPrice: number }>;
};
}
export interface LookupSiteInput {
siteId: string;
}
export interface LookupSiteOutput {
found: boolean;
site?: {
siteId: string;
name: string;
address?: string;
region?: string;
status: string;
assignedTechnicians?: string[];
};
}
// ---------------------------------------------------------------------------
// Tool factories — call makeTools(client) once at server startup
// ---------------------------------------------------------------------------
export function makeTools(client: InternalDataClient) {
const lookupWorkOrder = defineTool<LookupWorkOrderInput, LookupWorkOrderOutput>({
name: 'lookup_work_order',
description:
'Retrieve a Sea Haven work order by its ID. Returns current status, assigned site, ' +
'assigned technician, and description. Returns found=false when the ID does not exist.',
tier: 'ops',
requiredScope: 'ops:read',
inputSchema: {
type: 'object',
required: ['workOrderId'],
additionalProperties: false,
properties: {
workOrderId: {
type: 'string',
description: 'The unique work-order identifier (e.g. WO-20240101-001).',
minLength: 1,
maxLength: 128,
},
},
},
handler: async (
input: LookupWorkOrderInput,
ctx: AuthContext,
): Promise<LookupWorkOrderOutput> => {
requireScope(ctx, 'ops:read');
const record = await client.getWorkOrder(input.workOrderId);
if (record === null) {
return { found: false };
}
return {
found: true,
workOrder: {
workOrderId: record.workOrderId,
title: record.title,
status: record.status,
siteId: record.siteId,
assignedTo: record.assignedTo,
createdAt: record.createdAt,
updatedAt: record.updatedAt,
description: record.description,
},
};
},
});
const lookupPurchaseOrder = defineTool<LookupPurchaseOrderInput, LookupPurchaseOrderOutput>({
name: 'lookup_purchase_order',
description:
'Retrieve a Sea Haven purchase order by its ID. Returns vendor name, status, total amount, ' +
'and line items. Returns found=false when the ID does not exist.',
tier: 'ops',
requiredScope: 'ops:read',
inputSchema: {
type: 'object',
required: ['purchaseOrderId'],
additionalProperties: false,
properties: {
purchaseOrderId: {
type: 'string',
description: 'The unique purchase-order identifier (e.g. PO-2024-00123).',
minLength: 1,
maxLength: 128,
},
},
},
handler: async (
input: LookupPurchaseOrderInput,
ctx: AuthContext,
): Promise<LookupPurchaseOrderOutput> => {
requireScope(ctx, 'ops:read');
const record = await client.getPurchaseOrder(input.purchaseOrderId);
if (record === null) {
return { found: false };
}
return {
found: true,
purchaseOrder: {
purchaseOrderId: record.purchaseOrderId,
vendor: record.vendor,
status: record.status,
totalAmount: record.totalAmount,
currency: record.currency,
issuedAt: record.issuedAt,
updatedAt: record.updatedAt,
lineItems: record.lineItems,
},
};
},
});
const lookupSite = defineTool<LookupSiteInput, LookupSiteOutput>({
name: 'lookup_site',
description:
'Retrieve a Sea Haven site assignment record by its ID. Returns site name, address, ' +
'region, status, and assigned technicians. Returns found=false when the ID does not exist.',
tier: 'ops',
requiredScope: 'ops:read',
inputSchema: {
type: 'object',
required: ['siteId'],
additionalProperties: false,
properties: {
siteId: {
type: 'string',
description: 'The unique site identifier (e.g. SITE-NYC-001).',
minLength: 1,
maxLength: 128,
},
},
},
handler: async (
input: LookupSiteInput,
ctx: AuthContext,
): Promise<LookupSiteOutput> => {
requireScope(ctx, 'ops:read');
const record = await client.getSite(input.siteId);
if (record === null) {
return { found: false };
}
return {
found: true,
site: {
siteId: record.siteId,
name: record.name,
address: record.address,
region: record.region,
status: record.status,
assignedTechnicians: record.assignedTechnicians,
},
};
},
});
return [lookupWorkOrder, lookupPurchaseOrder, lookupSite] as const;
}