@isomorph.ai/app-sdk 1.37.0 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +26 -21
- package/dist/integration-execute-input-rules.d.ts +15 -0
- package/dist/integration-execute-input-rules.js +7 -0
- package/dist/integration-operations.d.ts +8 -0
- package/dist/integration-operations.js +10 -0
- package/dist/mcp-tools.d.ts +45 -0
- package/dist/mcp-tools.js +51 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -627,3 +627,28 @@ subfolders, shortcuts and native Google documents are excluded. No historical ba
|
|
|
627
627
|
Background read consent is required. Existing file-read size limits still apply. Provider
|
|
628
628
|
subscriptions and real upload acceptance must be verified after deployment. Retries retain
|
|
629
629
|
one durable job run; accounting/email side effects still need their own idempotency keys.
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
### Selected MCP tools (server actions and jobs)
|
|
633
|
+
|
|
634
|
+
Connect a compatible MCP server in Integrations, then select the app's tools with
|
|
635
|
+
`isomorph integrations tools select`. This writes accepted descriptors into the
|
|
636
|
+
existing `.isomorph/integrations.json`; `isomorph check` verifies literal usage.
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
const result = await isomorph.integrations.execute('finance', {
|
|
640
|
+
operation: 'mcp.tool.call', resource: 'server',
|
|
641
|
+
input: { tool: 'Invoices.Read', arguments: { id: invoiceId } },
|
|
642
|
+
});
|
|
643
|
+
const tools = await isomorph.integrations.tools('finance', {
|
|
644
|
+
tools: ['Invoices.Read', 'Invoices.Create'],
|
|
645
|
+
runId: persistedWorkflowId,
|
|
646
|
+
});
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
Use the provider's exact selected tool names. The helper loads accepted schemas
|
|
650
|
+
locally and returns AI SDK tools; it does not rediscover provider tools. For writes,
|
|
651
|
+
keep the workflow run ID and tool-call ID stable across retries. Direct writes
|
|
652
|
+
must supply their own persisted UUID `idempotencyKey`. A tool marked read-only
|
|
653
|
+
does not require a key. An incomplete replay requires reconciliation, not a new
|
|
654
|
+
write. Account consent and app permissions are enforced by the platform.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { McpCallRequest, McpCallResult, McpToolsOptions } from './mcp-tools.js';
|
|
2
|
+
export type { McpCallRequest, McpCallResult, McpToolDescriptor, McpSelectedTool, McpToolsOptions } from './mcp-tools.js';
|
|
1
3
|
import { type IsomorphUserRole } from './user-role.js';
|
|
2
4
|
export type { IsomorphUserRole } from './user-role.js';
|
|
3
5
|
export type IsomorphErrorCategory = 'AUTH_REQUIRED' | 'FORBIDDEN' | 'NOT_FOUND' | 'CONFLICT' | 'VALIDATION_FAILED' | 'RATE_LIMITED' | 'UNAVAILABLE' | 'INTERNAL';
|
|
@@ -1232,7 +1234,7 @@ export type ZohoBooksRequest = {
|
|
|
1232
1234
|
lineItems: BooksInvoiceLineInput[];
|
|
1233
1235
|
};
|
|
1234
1236
|
};
|
|
1235
|
-
export type IntegrationRequest = ZohoBooksRequest | ServiceRequest | SlackChannelHistoryRequest | SlackMessagePostRequest | SlackMessageUpdateRequest | SlackMessageDeleteRequest | SlackDmSendRequest | SlackUserLookupRequest | WarehouseViewReadRequest | WarehouseTableAppendRequest | PhoneSmsListRequest | PhoneNumberGetRequest | GmailThreadListRequest | GmailThreadReadRequest | GmailMessageReadRequest | GmailAttachmentReadRequest | GmailMessageSendRequest | BasecampProjectListRequest | BasecampTodolistListRequest | BasecampTodoListRequest | BasecampTodoCreateRequest | BasecampTodoCompleteRequest | BasecampMessageListRequest | BasecampMessagePostRequest | BasecampCommentListRequest | BasecampCommentPostRequest | BasecampCampfirePostRequest | BasecampPeopleListRequest | SharePointListListRequest | SharePointItemListRequest | SharePointItemCreateRequest | SharePointItemUpdateRequest | SharePointDriveListRequest | SharePointFileListRequest | SharePointFileSearchRequest | SharePointFileReadRequest | SharePointFileUploadRequest | CalendarCalendarListRequest | CalendarEventListRequest | CalendarEventReadRequest | CalendarEventCreateRequest | CalendarEventUpdateRequest | CalendarEventCancelRequest | DriveFileListRequest | DriveFileSearchRequest | DriveFileReadRequest | DriveFileUploadRequest | DriveFolderCreateRequest | DocsDocumentReadRequest | DocsDocumentCreateRequest | DocsDocumentAppendRequest | DocsTextReplaceRequest | SheetsSpreadsheetReadRequest | SheetsValuesReadRequest | SheetsValuesAppendRequest | SheetsValuesUpdateRequest | SheetsValuesClearRequest | SheetsSpreadsheetCreateRequest | OutlookMessageListRequest | OutlookMessageReadRequest | OutlookAttachmentReadRequest | OutlookMessageSendRequest | OutlookMessageReplyRequest | OutlookEventListRequest | OutlookEventReadRequest | OutlookEventCreateRequest | OutlookEventUpdateRequest | OutlookEventCancelRequest | OneDriveFileListRequest | OneDriveFileSearchRequest | OneDriveFileReadRequest | OneDriveFileUploadRequest | OneDriveFolderCreateRequest | ExcelWorksheetListRequest | ExcelRangeReadRequest | ExcelRangeUpdateRequest | ExcelTableListRequest | ExcelRowAddRequest | ExcelWorksheetAddRequest | TeamsMessageListRequest | TeamsMessagePostRequest | TeamsReplyPostRequest;
|
|
1237
|
+
export type IntegrationRequest = McpCallRequest | ZohoBooksRequest | ServiceRequest | SlackChannelHistoryRequest | SlackMessagePostRequest | SlackMessageUpdateRequest | SlackMessageDeleteRequest | SlackDmSendRequest | SlackUserLookupRequest | WarehouseViewReadRequest | WarehouseTableAppendRequest | PhoneSmsListRequest | PhoneNumberGetRequest | GmailThreadListRequest | GmailThreadReadRequest | GmailMessageReadRequest | GmailAttachmentReadRequest | GmailMessageSendRequest | BasecampProjectListRequest | BasecampTodolistListRequest | BasecampTodoListRequest | BasecampTodoCreateRequest | BasecampTodoCompleteRequest | BasecampMessageListRequest | BasecampMessagePostRequest | BasecampCommentListRequest | BasecampCommentPostRequest | BasecampCampfirePostRequest | BasecampPeopleListRequest | SharePointListListRequest | SharePointItemListRequest | SharePointItemCreateRequest | SharePointItemUpdateRequest | SharePointDriveListRequest | SharePointFileListRequest | SharePointFileSearchRequest | SharePointFileReadRequest | SharePointFileUploadRequest | CalendarCalendarListRequest | CalendarEventListRequest | CalendarEventReadRequest | CalendarEventCreateRequest | CalendarEventUpdateRequest | CalendarEventCancelRequest | DriveFileListRequest | DriveFileSearchRequest | DriveFileReadRequest | DriveFileUploadRequest | DriveFolderCreateRequest | DocsDocumentReadRequest | DocsDocumentCreateRequest | DocsDocumentAppendRequest | DocsTextReplaceRequest | SheetsSpreadsheetReadRequest | SheetsValuesReadRequest | SheetsValuesAppendRequest | SheetsValuesUpdateRequest | SheetsValuesClearRequest | SheetsSpreadsheetCreateRequest | OutlookMessageListRequest | OutlookMessageReadRequest | OutlookAttachmentReadRequest | OutlookMessageSendRequest | OutlookMessageReplyRequest | OutlookEventListRequest | OutlookEventReadRequest | OutlookEventCreateRequest | OutlookEventUpdateRequest | OutlookEventCancelRequest | OneDriveFileListRequest | OneDriveFileSearchRequest | OneDriveFileReadRequest | OneDriveFileUploadRequest | OneDriveFolderCreateRequest | ExcelWorksheetListRequest | ExcelRangeReadRequest | ExcelRangeUpdateRequest | ExcelTableListRequest | ExcelRowAddRequest | ExcelWorksheetAddRequest | TeamsMessageListRequest | TeamsMessagePostRequest | TeamsReplyPostRequest;
|
|
1236
1238
|
export type IntegrationOperation = IntegrationRequest['operation'];
|
|
1237
1239
|
export interface SlackMessage {
|
|
1238
1240
|
id: string;
|
|
@@ -1726,7 +1728,7 @@ export interface TeamsMessagePostResult {
|
|
|
1726
1728
|
id: string;
|
|
1727
1729
|
webUrl: string;
|
|
1728
1730
|
}
|
|
1729
|
-
export type IntegrationResult<R extends IntegrationRequest> = R extends {
|
|
1731
|
+
export type IntegrationResult<R extends IntegrationRequest> = R extends McpCallRequest ? McpCallResult : R extends {
|
|
1730
1732
|
operation: 'zohobooks.contact.list';
|
|
1731
1733
|
} ? {
|
|
1732
1734
|
booksContacts: {
|
|
@@ -2057,6 +2059,15 @@ export declare function createClient(options?: ClientOptions): {
|
|
|
2057
2059
|
page: (path?: string) => Promise<void>;
|
|
2058
2060
|
};
|
|
2059
2061
|
integrations: {
|
|
2062
|
+
tools: (connection: string, options: McpToolsOptions) => Promise<Record<string, import("ai").Tool<Record<string, unknown>, {
|
|
2063
|
+
content?: Array<Record<string, unknown>>;
|
|
2064
|
+
structuredContent?: Record<string, unknown>;
|
|
2065
|
+
isError?: boolean;
|
|
2066
|
+
replay?: {
|
|
2067
|
+
complete: boolean;
|
|
2068
|
+
summary?: string;
|
|
2069
|
+
};
|
|
2070
|
+
}>>>;
|
|
2060
2071
|
execute: <R extends IntegrationRequest>(connection: string, request: R) => Promise<IntegrationResult<R>>;
|
|
2061
2072
|
connect: (connection: string) => Promise<IntegrationConnectResult>;
|
|
2062
2073
|
disconnect: (connection: string) => Promise<IntegrationDisconnectResult>;
|
package/dist/index.js
CHANGED
|
@@ -459,6 +459,28 @@ export function createClient(options = {}) {
|
|
|
459
459
|
return user;
|
|
460
460
|
};
|
|
461
461
|
const transport = new Transport(options, () => notifyIdentity(null));
|
|
462
|
+
const executeIntegration = (connection, request) => {
|
|
463
|
+
// The contract's per-operation rules, refused here before any request
|
|
464
|
+
// is made, with the platform's own sentence and the field named — so
|
|
465
|
+
// `isomorph check` (which runs retained journeys through this client)
|
|
466
|
+
// and the deployed app fail the same way at build time, not at runtime.
|
|
467
|
+
const checked = validateIntegrationExecuteInput(request.operation, request.input);
|
|
468
|
+
const problem = (request.operation === 'warehouse.table.append' ? warehouseAppendProblem(request) : undefined)
|
|
469
|
+
?? (!checked.ok ? checked.problem
|
|
470
|
+
: request.operation === 'warehouse.view.read' ? warehouseReadProblem(request.input ?? {}) : undefined);
|
|
471
|
+
if (problem) {
|
|
472
|
+
return Promise.reject(new IsomorphError('VALIDATION_FAILED', problem.message, undefined, { code: 'INPUT_INVALID', field: problem.field }));
|
|
473
|
+
}
|
|
474
|
+
// Exactly the contract's fields go on the wire: metadata declares which
|
|
475
|
+
// operations accept `mode`; identity and stray fields are not sent.
|
|
476
|
+
// phone.number.get takes no input, but the contract keeps `input`
|
|
477
|
+
// required on the wire (ADR 0029), so an omitted one goes out as {}.
|
|
478
|
+
const { operation, resource, input: given, idempotencyKey, mode } = request;
|
|
479
|
+
const input = given === undefined && operation === 'phone.number.get' ? {} : given;
|
|
480
|
+
return transport.json('/_harbour/integrations/execute', {
|
|
481
|
+
method: 'POST', body: JSON.stringify({ connection, operation, ...(operation !== 'service.request' ? { resource } : {}), ...(input !== undefined ? { input } : {}), ...(idempotencyKey !== undefined ? { idempotencyKey } : {}), ...(IntegrationOperations[operation]?.declaredMode && mode !== undefined ? { mode } : {}) }),
|
|
482
|
+
});
|
|
483
|
+
};
|
|
462
484
|
const socketFactory = options.websocket ?? ((url) => defaultSocketFactory(url, options.identity));
|
|
463
485
|
const refreshIdentity = () => {
|
|
464
486
|
if (refreshInFlight)
|
|
@@ -805,28 +827,11 @@ export function createClient(options = {}) {
|
|
|
805
827
|
}),
|
|
806
828
|
},
|
|
807
829
|
integrations: {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
// `isomorph check` (which runs retained journeys through this client)
|
|
812
|
-
// and the deployed app fail the same way at build time, not at runtime.
|
|
813
|
-
const checked = validateIntegrationExecuteInput(request.operation, request.input);
|
|
814
|
-
const problem = (request.operation === 'warehouse.table.append' ? warehouseAppendProblem(request) : undefined)
|
|
815
|
-
?? (!checked.ok ? checked.problem
|
|
816
|
-
: request.operation === 'warehouse.view.read' ? warehouseReadProblem(request.input ?? {}) : undefined);
|
|
817
|
-
if (problem) {
|
|
818
|
-
return Promise.reject(new IsomorphError('VALIDATION_FAILED', problem.message, undefined, { code: 'INPUT_INVALID', field: problem.field }));
|
|
819
|
-
}
|
|
820
|
-
// Exactly the contract's fields go on the wire: metadata declares which
|
|
821
|
-
// operations accept `mode`; identity and stray fields are not sent.
|
|
822
|
-
// phone.number.get takes no input, but the contract keeps `input`
|
|
823
|
-
// required on the wire (ADR 0029), so an omitted one goes out as {}.
|
|
824
|
-
const { operation, resource, input: given, idempotencyKey, mode } = request;
|
|
825
|
-
const input = given === undefined && operation === 'phone.number.get' ? {} : given;
|
|
826
|
-
return transport.json('/_harbour/integrations/execute', {
|
|
827
|
-
method: 'POST', body: JSON.stringify({ connection, operation, ...(operation !== 'service.request' ? { resource } : {}), ...(input !== undefined ? { input } : {}), ...(idempotencyKey !== undefined ? { idempotencyKey } : {}), ...(IntegrationOperations[operation]?.declaredMode && mode !== undefined ? { mode } : {}) }),
|
|
828
|
-
});
|
|
830
|
+
tools: async (connection, options) => {
|
|
831
|
+
const { createMcpTools } = await import('./mcp-tools.js');
|
|
832
|
+
return createMcpTools(connection, options, request => executeIntegration(connection, request));
|
|
829
833
|
},
|
|
834
|
+
execute: executeIntegration,
|
|
830
835
|
// In a browser the consent returns the person to this page (without its
|
|
831
836
|
// fragment); governance accepts it only as a registered page of this app
|
|
832
837
|
// (ADR 0030). Outside a browser there is no page, and none is sent.
|
|
@@ -2556,6 +2556,21 @@ export declare const IntegrationExecuteInputRules: {
|
|
|
2556
2556
|
};
|
|
2557
2557
|
};
|
|
2558
2558
|
};
|
|
2559
|
+
readonly "mcp.tool.call": {
|
|
2560
|
+
readonly idempotencyKey: false;
|
|
2561
|
+
readonly fields: {
|
|
2562
|
+
readonly tool: {
|
|
2563
|
+
readonly kind: "string";
|
|
2564
|
+
readonly required: true;
|
|
2565
|
+
readonly minLength: 1;
|
|
2566
|
+
readonly maxLength: 128;
|
|
2567
|
+
};
|
|
2568
|
+
readonly arguments: {
|
|
2569
|
+
readonly kind: "object";
|
|
2570
|
+
readonly required: true;
|
|
2571
|
+
};
|
|
2572
|
+
};
|
|
2573
|
+
};
|
|
2559
2574
|
};
|
|
2560
2575
|
export type IntegrationExecuteOperation = keyof typeof IntegrationExecuteInputRules;
|
|
2561
2576
|
/** The upper bound of one integer input as a literal type: integrationExecuteInputMaximum('slack.channel.history', 'limit') is 15. */
|
|
@@ -792,6 +792,13 @@ export const IntegrationExecuteInputRules = {
|
|
|
792
792
|
documentId: { kind: "string", required: true, minLength: 0, maxLength: 32, pattern: "^[0-9]{1,32}$" },
|
|
793
793
|
},
|
|
794
794
|
},
|
|
795
|
+
"mcp.tool.call": {
|
|
796
|
+
idempotencyKey: false,
|
|
797
|
+
fields: {
|
|
798
|
+
tool: { kind: "string", required: true, minLength: 1, maxLength: 128 },
|
|
799
|
+
arguments: { kind: "object", required: true },
|
|
800
|
+
},
|
|
801
|
+
},
|
|
795
802
|
};
|
|
796
803
|
/** The upper bound of one integer input as a literal type: integrationExecuteInputMaximum('slack.channel.history', 'limit') is 15. */
|
|
797
804
|
export function integrationExecuteInputMaximum(operation, field) {
|
|
@@ -665,4 +665,12 @@ export declare const IntegrationOperations: {
|
|
|
665
665
|
readonly declaredMode: false;
|
|
666
666
|
readonly idempotencyKey: false;
|
|
667
667
|
};
|
|
668
|
+
readonly "mcp.tool.call": {
|
|
669
|
+
readonly kind: "mcp";
|
|
670
|
+
readonly identities: readonly ["user"];
|
|
671
|
+
readonly readOnly: false;
|
|
672
|
+
readonly declaredMode: false;
|
|
673
|
+
readonly idempotencyKey: false;
|
|
674
|
+
readonly runtimeIdempotency: true;
|
|
675
|
+
};
|
|
668
676
|
};
|
|
@@ -860,5 +860,15 @@ export const IntegrationOperations = {
|
|
|
860
860
|
"readOnly": true,
|
|
861
861
|
"declaredMode": false,
|
|
862
862
|
"idempotencyKey": false
|
|
863
|
+
},
|
|
864
|
+
"mcp.tool.call": {
|
|
865
|
+
"kind": "mcp",
|
|
866
|
+
"identities": [
|
|
867
|
+
"user"
|
|
868
|
+
],
|
|
869
|
+
"readOnly": false,
|
|
870
|
+
"declaredMode": false,
|
|
871
|
+
"idempotencyKey": false,
|
|
872
|
+
"runtimeIdempotency": true
|
|
863
873
|
}
|
|
864
874
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Tool } from 'ai';
|
|
2
|
+
export interface McpToolDescriptor {
|
|
3
|
+
name: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
inputSchema: Record<string, unknown>;
|
|
7
|
+
outputSchema?: Record<string, unknown>;
|
|
8
|
+
annotations?: {
|
|
9
|
+
readOnlyHint?: boolean;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface McpSelectedTool {
|
|
14
|
+
name: string;
|
|
15
|
+
contractHash: string;
|
|
16
|
+
descriptor: McpToolDescriptor;
|
|
17
|
+
}
|
|
18
|
+
export interface McpCallRequest {
|
|
19
|
+
operation: 'mcp.tool.call';
|
|
20
|
+
resource: 'server';
|
|
21
|
+
mode?: 'user';
|
|
22
|
+
input: {
|
|
23
|
+
tool: string;
|
|
24
|
+
arguments: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
idempotencyKey?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface McpCallResult {
|
|
29
|
+
mcp: {
|
|
30
|
+
content?: Array<Record<string, unknown>>;
|
|
31
|
+
structuredContent?: Record<string, unknown>;
|
|
32
|
+
isError?: boolean;
|
|
33
|
+
replay?: {
|
|
34
|
+
complete: boolean;
|
|
35
|
+
summary?: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export interface McpToolsOptions {
|
|
40
|
+
tools: readonly string[];
|
|
41
|
+
runId: string;
|
|
42
|
+
}
|
|
43
|
+
type Execute = (request: McpCallRequest) => Promise<McpCallResult>;
|
|
44
|
+
export declare function createMcpTools(connection: string, options: McpToolsOptions, execute: Execute): Promise<Record<string, Tool<Record<string, unknown>, McpCallResult['mcp']>>>;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
async function digest(parts) {
|
|
2
|
+
return new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(parts))));
|
|
3
|
+
}
|
|
4
|
+
function hex(bytes) {
|
|
5
|
+
return Array.from(bytes, value => value.toString(16).padStart(2, '0')).join('');
|
|
6
|
+
}
|
|
7
|
+
async function invocationKey(runId, tool, callId) {
|
|
8
|
+
const bytes = (await digest([runId, tool, callId])).slice(0, 16);
|
|
9
|
+
bytes[6] = (bytes[6] & 15) | 128;
|
|
10
|
+
bytes[8] = (bytes[8] & 63) | 128;
|
|
11
|
+
const value = hex(bytes);
|
|
12
|
+
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
|
|
13
|
+
}
|
|
14
|
+
export async function createMcpTools(connection, options, execute) {
|
|
15
|
+
if (typeof window !== 'undefined' && typeof window.document !== 'undefined')
|
|
16
|
+
throw new Error('MCP agent tools run in actions or jobs.');
|
|
17
|
+
if (!options.runId || !Array.isArray(options.tools) || options.tools.length < 1 || options.tools.length > 64 || new Set(options.tools).size !== options.tools.length)
|
|
18
|
+
throw new Error('MCP tools need a durable runId and 1–64 unique selected tool names.');
|
|
19
|
+
const [{ readFile }, { resolve }, { jsonSchema }] = await Promise.all([import('node:fs/promises'), import('node:path'), import('ai')]);
|
|
20
|
+
const root = process.env.ISOMORPH_APP_ROOT || process.cwd();
|
|
21
|
+
const declaration = JSON.parse(await readFile(resolve(root, '.isomorph/integrations.json'), 'utf8'));
|
|
22
|
+
const source = declaration.connections[connection];
|
|
23
|
+
if (declaration.schema !== 'isomorph.app-integrations/2.0' || source?.kind !== 'mcp' || !source.mcp)
|
|
24
|
+
throw new Error('This MCP connection is not declared for the app. Run the integration selection and checks.');
|
|
25
|
+
const allowed = new Map(source.mcp.tools.map(tool => [tool.name, tool]));
|
|
26
|
+
const result = {};
|
|
27
|
+
for (const name of options.tools) {
|
|
28
|
+
const selected = allowed.get(name);
|
|
29
|
+
if (!selected || selected.descriptor.name !== name)
|
|
30
|
+
throw new Error(`MCP tool ${name} is not declared for this app.`);
|
|
31
|
+
const descriptor = selected.descriptor;
|
|
32
|
+
const alias = `mcp_${hex((await digest([connection, name])).slice(0, 20))}`;
|
|
33
|
+
result[alias] = {
|
|
34
|
+
description: descriptor.description ?? descriptor.title ?? name,
|
|
35
|
+
inputSchema: jsonSchema(descriptor.inputSchema),
|
|
36
|
+
execute: async (input, context) => {
|
|
37
|
+
const readOnly = descriptor.annotations?.readOnlyHint === true;
|
|
38
|
+
if (!readOnly && !context.toolCallId)
|
|
39
|
+
throw new Error('MCP writes need a persisted toolCallId. Do not retry a write with a new invocation ID.');
|
|
40
|
+
const response = await execute({
|
|
41
|
+
operation: 'mcp.tool.call', resource: 'server', mode: 'user', input: { tool: name, arguments: input },
|
|
42
|
+
...(!readOnly ? { idempotencyKey: await invocationKey(options.runId, name, context.toolCallId) } : {}),
|
|
43
|
+
});
|
|
44
|
+
if (response.mcp.replay?.complete === false)
|
|
45
|
+
throw new Error('This MCP action already completed, but its result was not retained. Reconcile the provider record before continuing; do not repeat the action.');
|
|
46
|
+
return response.mcp;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|