mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-02 14:13:16 +00:00
229 lines
6.4 KiB
TypeScript
229 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;
|
||
|
|
}
|