@isomorph.ai/app-sdk 1.37.0 → 1.38.1

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 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, McpAgentTool } 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,7 @@ 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("./mcp-tools.js").McpAgentTool>>;
2060
2063
  execute: <R extends IntegrationRequest>(connection: string, request: R) => Promise<IntegrationResult<R>>;
2061
2064
  connect: (connection: string) => Promise<IntegrationConnectResult>;
2062
2065
  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
- execute: (connection, request) => {
809
- // The contract's per-operation rules, refused here before any request
810
- // is made, with the platform's own sentence and the field named — so
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,53 @@
1
+ export interface McpToolDescriptor {
2
+ name: string;
3
+ description?: string;
4
+ title?: string;
5
+ inputSchema: Record<string, unknown>;
6
+ outputSchema?: Record<string, unknown>;
7
+ annotations?: {
8
+ readOnlyHint?: boolean;
9
+ [key: string]: unknown;
10
+ };
11
+ }
12
+ export interface McpSelectedTool {
13
+ name: string;
14
+ contractHash: string;
15
+ descriptor: McpToolDescriptor;
16
+ }
17
+ export interface McpCallRequest {
18
+ operation: 'mcp.tool.call';
19
+ resource: 'server';
20
+ mode?: 'user';
21
+ input: {
22
+ tool: string;
23
+ arguments: Record<string, unknown>;
24
+ };
25
+ idempotencyKey?: string;
26
+ }
27
+ export interface McpCallResult {
28
+ mcp: {
29
+ content?: Array<Record<string, unknown>>;
30
+ structuredContent?: Record<string, unknown>;
31
+ isError?: boolean;
32
+ replay?: {
33
+ complete: boolean;
34
+ summary?: string;
35
+ };
36
+ };
37
+ }
38
+ export interface McpToolsOptions {
39
+ tools: readonly string[];
40
+ runId: string;
41
+ }
42
+ /** The schema object belongs to the optional AI SDK and carries its private brand.
43
+ * Keep that implementation type out of ordinary clients' declaration graph. */
44
+ export interface McpAgentTool {
45
+ description?: string;
46
+ inputSchema: any;
47
+ execute(input: Record<string, unknown>, context: {
48
+ toolCallId: string;
49
+ }): Promise<McpCallResult['mcp']>;
50
+ }
51
+ type Execute = (request: McpCallRequest) => Promise<McpCallResult>;
52
+ export declare function createMcpTools(connection: string, options: McpToolsOptions, execute: Execute): Promise<Record<string, McpAgentTool>>;
53
+ 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/app-sdk",
3
- "version": "1.37.0",
3
+ "version": "1.38.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",