@opengeni/capabilities 0.2.0 → 0.3.1-canary.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.
@@ -43,7 +43,6 @@ export interface IntegrationFacetDefinition {
43
43
  readonly capabilities: Readonly<Record<string, unknown>>;
44
44
  }
45
45
  export declare const GOOGLE_DRIVE_INTEGRATION_DEFINITION: IntegrationDefinition;
46
- export declare const GOOGLE_GMAIL_INTEGRATION_DEFINITION: IntegrationDefinition;
47
46
  export declare const MICROSOFT_GRAPH_OPENAPI_URL = "https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/openapi/v1.0/openapi.yaml";
48
47
  export declare const MICROSOFT_GRAPH_BASE_URL = "https://graph.microsoft.com/v1.0";
49
48
  export declare const MICROSOFT_OUTLOOK_MAIL_INTEGRATION_DEFINITION: IntegrationDefinition;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Reviewed consent copy for the core API integration definitions.
3
+ *
4
+ * Presentation only: nothing here grants a scope, selects a connection, or
5
+ * replaces server-side authorization. The copy used to live hardcoded in the
6
+ * web bundle (`REVIEWED_INTEGRATION_EXPERIENCES`); it is served with the
7
+ * definition now so polishing a consent screen is a data change, not a
8
+ * frontend release. The web keeps its generic fallback for any definition or
9
+ * field missing here. Gmail's reviewed presentation lives on its catalog row
10
+ * instead (`data/catalog/curated.json`): Gmail is a Connector, not one of
11
+ * these API integration definitions.
12
+ *
13
+ * MCP connectors carry the same shape on their curated catalog row
14
+ * (`presentation` in `data/catalog/curated.json` -> importer ->
15
+ * `capability_catalog_items.metadata.presentation`).
16
+ */
17
+ export type IntegrationPresentationIcon = "calendar" | "cloud" | "contacts" | "files" | "mail";
18
+ export type IntegrationPresentationCopy = {
19
+ readonly providerName?: string;
20
+ readonly icon?: IntegrationPresentationIcon;
21
+ readonly introduction?: string;
22
+ readonly capabilities?: readonly {
23
+ readonly title: string;
24
+ readonly description: string;
25
+ }[];
26
+ readonly permissionSummary?: string;
27
+ readonly scopeLabels?: Readonly<Record<string, {
28
+ readonly label: string;
29
+ readonly description: string;
30
+ }>>;
31
+ };
32
+ export declare const INTEGRATION_DEFINITION_PRESENTATIONS: Readonly<Record<string, IntegrationPresentationCopy>>;
@@ -0,0 +1,41 @@
1
+ import type { MCPServer } from "@openai/agents";
2
+ export declare const LOCAL_MCP_BRIDGE_CONTRACT_VERSION: 1;
3
+ export type LocalMcpBridgeAuthority = "connection" | "host" | "none";
4
+ export type LocalMcpBridgeToolSurface = "static_reviewed";
5
+ export type LocalMcpBridgeDestination = Readonly<{
6
+ origin: string;
7
+ pathPrefix: string;
8
+ }>;
9
+ /**
10
+ * Secret-free description of an in-process provider-to-MCP adapter.
11
+ *
12
+ * This is observability and registration metadata, not authorization. The
13
+ * adapter must still revalidate its named authority before each physical
14
+ * provider request and keep credentials outside tool results and schemas.
15
+ */
16
+ export type LocalMcpBridgeDescriptor = Readonly<{
17
+ contractVersion: typeof LOCAL_MCP_BRIDGE_CONTRACT_VERSION;
18
+ adapterId: string;
19
+ providerId: string;
20
+ catalogIdentity: string;
21
+ transport: "in_process";
22
+ authority: LocalMcpBridgeAuthority;
23
+ toolSurface: LocalMcpBridgeToolSurface;
24
+ mutationReplay: "safe_reads_only";
25
+ destinations: readonly LocalMcpBridgeDestination[];
26
+ }>;
27
+ export interface LocalMcpBridgeServer extends MCPServer {
28
+ readonly bridge: LocalMcpBridgeDescriptor;
29
+ }
30
+ export interface LocalMcpBridgeAdapter<TConfig, TContext> {
31
+ readonly adapterId: string;
32
+ matches(config: TConfig): boolean;
33
+ create(config: TConfig, context: TContext): LocalMcpBridgeServer;
34
+ }
35
+ export declare function defineLocalMcpBridgeDescriptor(input: Omit<LocalMcpBridgeDescriptor, "contractVersion" | "transport">): LocalMcpBridgeDescriptor;
36
+ export declare function isLocalMcpBridgeServer(server: MCPServer): server is LocalMcpBridgeServer;
37
+ /**
38
+ * Select exactly one adapter for a runtime catalog row. Ambiguous matches fail
39
+ * closed so adding a bridge cannot silently replace another provider route.
40
+ */
41
+ export declare function createLocalMcpBridgeFromAdapters<TConfig, TContext>(adapters: readonly LocalMcpBridgeAdapter<TConfig, TContext>[], config: TConfig, context: TContext): LocalMcpBridgeServer | null;
package/dist/types.d.ts CHANGED
@@ -30,6 +30,8 @@ export interface IntegrationCredentialAudience {
30
30
  export interface ResolvedIntegrationCredential {
31
31
  readonly audience: IntegrationCredentialAudience;
32
32
  readonly placements: readonly IntegrationCredentialPlacement[];
33
+ /** Exact accepted-attempt fence invoked immediately before one HTTP request. */
34
+ readonly authorizeProviderRequest?: () => Promise<boolean>;
33
35
  readonly expiresAt?: string;
34
36
  readonly scope?: readonly string[];
35
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/capabilities",
3
- "version": "0.2.0",
3
+ "version": "0.3.1-canary.0",
4
4
  "description": "Protocol-neutral capability integration compilers and local MCP adapters for OpenGeni.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@openai/agents": "0.14.3",
39
- "@opengeni/network": "^0.2.2",
39
+ "@opengeni/network": "^0.2.3-canary.0",
40
40
  "graphql": "^16.12.0",
41
41
  "js-yaml": "4.1.1"
42
42
  },
package/src/graphql.ts CHANGED
@@ -428,6 +428,22 @@ async function sendGraphqlRequest(
428
428
  headers.set("accept", "application/json");
429
429
  headers.set("content-type", "application/json");
430
430
  if (credential) applyCredentialPlacements(endpoint, headers, credential);
431
+ if (credential?.authorizeProviderRequest) {
432
+ let authorized = false;
433
+ try {
434
+ authorized = await credential.authorizeProviderRequest();
435
+ } catch {
436
+ authorized = false;
437
+ }
438
+ if (!authorized) {
439
+ throw new IntegrationInvocationError(
440
+ "authorization_rejected",
441
+ "The connected account is no longer authorized for this operation",
442
+ "not_started",
443
+ false,
444
+ );
445
+ }
446
+ }
431
447
  return await fetchWithDeadline(
432
448
  options.transport,
433
449
  endpoint,
package/src/index.ts CHANGED
@@ -2,7 +2,9 @@ export * from "./auth";
2
2
  export * from "./graphql";
3
3
  export * from "./http";
4
4
  export * from "./mcp-manifest";
5
+ export * from "./mcp-bridge";
5
6
  export * from "./openapi";
6
7
  export * from "./integration-definitions";
8
+ export * from "./integration-presentations";
7
9
  export * from "./revision";
8
10
  export * from "./types";
@@ -123,7 +123,7 @@ const driveKnowledgeFacet = (
123
123
  });
124
124
 
125
125
  const mailboxFacets = (
126
- provider: "google-gmail" | "microsoft-outlook-mail",
126
+ provider: "microsoft-outlook-mail",
127
127
  ): readonly IntegrationFacetDefinition[] => [
128
128
  {
129
129
  facetKey: "mail-inbox",
@@ -140,7 +140,7 @@ const mailboxFacets = (
140
140
  provider,
141
141
  connectionRequired: true,
142
142
  delivery: "poll",
143
- cursor: provider === "google-gmail" ? "history_id" : "delta_link",
143
+ cursor: "delta_link",
144
144
  },
145
145
  },
146
146
  {
@@ -160,7 +160,7 @@ const mailboxFacets = (
160
160
  delivery: "email",
161
161
  },
162
162
  },
163
- accountIdentityFacet(provider === "google-gmail" ? "google" : "microsoft"),
163
+ accountIdentityFacet("microsoft"),
164
164
  ];
165
165
 
166
166
  const googleDiscoveryUrl = (service: string, version: string): string =>
@@ -191,22 +191,6 @@ export const GOOGLE_DRIVE_INTEGRATION_DEFINITION: IntegrationDefinition = {
191
191
  facets: [driveKnowledgeFacet("google-drive"), accountIdentityFacet("google")],
192
192
  };
193
193
 
194
- export const GOOGLE_GMAIL_INTEGRATION_DEFINITION: IntegrationDefinition = {
195
- id: "google-gmail",
196
- name: "Gmail",
197
- summary: "Messages, threads, labels, drafts, and sending mail.",
198
- protocol: "openapi",
199
- provider: { id: "google", domain: "gmail.googleapis.com" },
200
- source: { kind: "google_discovery", url: googleDiscoveryUrl("gmail", "v1") },
201
- baseUrl: "https://gmail.googleapis.com/",
202
- authentication: googleOAuth(["https://mail.google.com/"]),
203
- healthCheck: {
204
- operationKey: "gmail.users.labels.list",
205
- arguments: { path: { userId: "me" } },
206
- },
207
- facets: mailboxFacets("google-gmail"),
208
- };
209
-
210
194
  export const MICROSOFT_GRAPH_OPENAPI_URL =
211
195
  "https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/openapi/v1.0/openapi.yaml";
212
196
  export const MICROSOFT_GRAPH_BASE_URL = "https://graph.microsoft.com/v1.0";
@@ -338,7 +322,6 @@ export const MICROSOFT_ONEDRIVE_INTEGRATION_DEFINITION: IntegrationDefinition =
338
322
 
339
323
  export const CORE_INTEGRATION_DEFINITIONS: readonly IntegrationDefinition[] = [
340
324
  GOOGLE_DRIVE_INTEGRATION_DEFINITION,
341
- GOOGLE_GMAIL_INTEGRATION_DEFINITION,
342
325
  MICROSOFT_OUTLOOK_MAIL_INTEGRATION_DEFINITION,
343
326
  MICROSOFT_OUTLOOK_CALENDAR_INTEGRATION_DEFINITION,
344
327
  MICROSOFT_OUTLOOK_CONTACTS_INTEGRATION_DEFINITION,
@@ -480,23 +463,23 @@ function collectGoogleMethods(
480
463
  const path = stringValue(rawMethod.path);
481
464
  const httpMethod = stringValue(rawMethod.httpMethod)?.toLowerCase();
482
465
  if (!path || !httpMethod) continue;
483
- const parameters = Object.entries(
484
- isRecord(rawMethod.parameters) ? rawMethod.parameters : {},
485
- ).flatMap(([name, rawParameter]): Record<string, unknown>[] => {
486
- if (!isRecord(rawParameter)) return [];
487
- const location = rawParameter.location === "path" ? "path" : "query";
488
- return [
489
- {
490
- name,
491
- in: location,
492
- required: location === "path" || rawParameter.required === true,
493
- ...(stringValue(rawParameter.description)
494
- ? { description: stringValue(rawParameter.description) }
495
- : {}),
496
- schema: convertGoogleSchema(rawParameter),
497
- },
498
- ];
499
- });
466
+ const parameters = Object.entries(isRecord(rawMethod.parameters) ? rawMethod.parameters : {})
467
+ .sort(([left], [right]) => left.localeCompare(right))
468
+ .flatMap(([name, rawParameter]): Record<string, unknown>[] => {
469
+ if (!isRecord(rawParameter)) return [];
470
+ const location = rawParameter.location === "path" ? "path" : "query";
471
+ return [
472
+ {
473
+ name,
474
+ in: location,
475
+ required: location === "path" || rawParameter.required === true,
476
+ ...(stringValue(rawParameter.description)
477
+ ? { description: stringValue(rawParameter.description) }
478
+ : {}),
479
+ schema: convertGoogleSchema(rawParameter),
480
+ },
481
+ ];
482
+ });
500
483
  const requestRef = isRecord(rawMethod.request)
501
484
  ? stringValue(rawMethod.request.$ref)
502
485
  : undefined;
@@ -505,7 +488,10 @@ function collectGoogleMethods(
505
488
  : undefined;
506
489
  const operation: Record<string, unknown> = {
507
490
  operationId: stringValue(rawMethod.id) ?? fallbackId,
508
- summary: stringValue(rawMethod.description) ?? stringValue(rawMethod.id) ?? fallbackId,
491
+ // Discovery descriptions are often full documentation paragraphs. Keep
492
+ // them as descriptions and use the stable method identity for the short
493
+ // OpenGeni tool display name.
494
+ summary: stringValue(rawMethod.id) ?? fallbackId,
509
495
  description: stringValue(rawMethod.description),
510
496
  parameters,
511
497
  responses: {
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Reviewed consent copy for the core API integration definitions.
3
+ *
4
+ * Presentation only: nothing here grants a scope, selects a connection, or
5
+ * replaces server-side authorization. The copy used to live hardcoded in the
6
+ * web bundle (`REVIEWED_INTEGRATION_EXPERIENCES`); it is served with the
7
+ * definition now so polishing a consent screen is a data change, not a
8
+ * frontend release. The web keeps its generic fallback for any definition or
9
+ * field missing here. Gmail's reviewed presentation lives on its catalog row
10
+ * instead (`data/catalog/curated.json`): Gmail is a Connector, not one of
11
+ * these API integration definitions.
12
+ *
13
+ * MCP connectors carry the same shape on their curated catalog row
14
+ * (`presentation` in `data/catalog/curated.json` -> importer ->
15
+ * `capability_catalog_items.metadata.presentation`).
16
+ */
17
+
18
+ export type IntegrationPresentationIcon = "calendar" | "cloud" | "contacts" | "files" | "mail";
19
+
20
+ export type IntegrationPresentationCopy = {
21
+ readonly providerName?: string;
22
+ readonly icon?: IntegrationPresentationIcon;
23
+ readonly introduction?: string;
24
+ readonly capabilities?: readonly { readonly title: string; readonly description: string }[];
25
+ readonly permissionSummary?: string;
26
+ readonly scopeLabels?: Readonly<
27
+ Record<string, { readonly label: string; readonly description: string }>
28
+ >;
29
+ };
30
+
31
+ export const INTEGRATION_DEFINITION_PRESENTATIONS: Readonly<
32
+ Record<string, IntegrationPresentationCopy>
33
+ > = {
34
+ "google-drive": {
35
+ providerName: "Google",
36
+ icon: "files",
37
+ introduction: "Let agents work with files in the Google Drive account you choose.",
38
+ capabilities: [
39
+ {
40
+ title: "Find files and folders",
41
+ description: "Browse and search content in My Drive and shared drives.",
42
+ },
43
+ {
44
+ title: "Create and update content",
45
+ description: "Work with files and folders through the reviewed Drive tools.",
46
+ },
47
+ {
48
+ title: "Manage sharing",
49
+ description: "Review and update links, permissions, and shared-drive content.",
50
+ },
51
+ ],
52
+ permissionSummary:
53
+ "Google asks for access to the Drive account you approve, including files shared with that account.",
54
+ scopeLabels: {
55
+ "https://www.googleapis.com/auth/drive": {
56
+ label: "Work with Google Drive files",
57
+ description: "See, create, edit, organize, and share files available to this account.",
58
+ },
59
+ },
60
+ },
61
+ "microsoft-outlook-mail": {
62
+ providerName: "Microsoft",
63
+ icon: "mail",
64
+ introduction: "Let agents work with mail in the Microsoft account you choose.",
65
+ capabilities: [
66
+ {
67
+ title: "Find and understand mail",
68
+ description: "Search messages, folders, and attachments for useful context.",
69
+ },
70
+ {
71
+ title: "Draft and send messages",
72
+ description: "Prepare, update, and send mail through the reviewed Outlook tools.",
73
+ },
74
+ {
75
+ title: "Manage mailbox settings",
76
+ description: "Work with supported folders, classifications, and mailbox preferences.",
77
+ },
78
+ ],
79
+ permissionSummary:
80
+ "Microsoft asks for mail and mailbox-setting access for the account you approve.",
81
+ scopeLabels: {
82
+ "Mail.ReadWrite": {
83
+ label: "Read and update mail",
84
+ description: "Work with messages, folders, and attachments in this mailbox.",
85
+ },
86
+ "Mail.Send": {
87
+ label: "Send mail",
88
+ description: "Send messages as the connected Microsoft account.",
89
+ },
90
+ "MailboxSettings.ReadWrite": {
91
+ label: "Manage mailbox settings",
92
+ description: "Read and update supported Outlook mailbox preferences.",
93
+ },
94
+ },
95
+ },
96
+ "microsoft-outlook-calendar": {
97
+ providerName: "Microsoft",
98
+ icon: "calendar",
99
+ introduction: "Let agents help coordinate the calendars in your Microsoft account.",
100
+ capabilities: [
101
+ {
102
+ title: "Understand your schedule",
103
+ description: "Review calendars, events, availability, and reminders.",
104
+ },
105
+ {
106
+ title: "Plan meetings",
107
+ description: "Find suitable times and coordinate calendar activity.",
108
+ },
109
+ {
110
+ title: "Manage events",
111
+ description: "Create and update events through the reviewed calendar tools.",
112
+ },
113
+ ],
114
+ permissionSummary:
115
+ "Microsoft asks for permission to view and manage calendars for the account you approve.",
116
+ scopeLabels: {
117
+ "Calendars.ReadWrite": {
118
+ label: "View and manage calendars",
119
+ description: "Read, create, update, and organize calendar events.",
120
+ },
121
+ },
122
+ },
123
+ "microsoft-outlook-contacts": {
124
+ providerName: "Microsoft",
125
+ icon: "contacts",
126
+ introduction: "Let agents work with contacts in your Microsoft account.",
127
+ capabilities: [
128
+ {
129
+ title: "Find people",
130
+ description: "Look up contacts and relevant people suggestions.",
131
+ },
132
+ {
133
+ title: "Organize contacts",
134
+ description: "Work with contacts and contact folders.",
135
+ },
136
+ {
137
+ title: "Keep details current",
138
+ description: "Create or update contact information through reviewed tools.",
139
+ },
140
+ ],
141
+ permissionSummary:
142
+ "Microsoft asks for contact access and people suggestions for the account you approve.",
143
+ scopeLabels: {
144
+ "Contacts.ReadWrite": {
145
+ label: "View and manage contacts",
146
+ description: "Read, create, update, and organize contacts and contact folders.",
147
+ },
148
+ "People.Read.All": {
149
+ label: "Find relevant people",
150
+ description: "Use people suggestions available to the connected account.",
151
+ },
152
+ },
153
+ },
154
+ "microsoft-onedrive": {
155
+ providerName: "Microsoft",
156
+ icon: "cloud",
157
+ introduction: "Let agents work with files in the Microsoft account you choose.",
158
+ capabilities: [
159
+ {
160
+ title: "Find files and folders",
161
+ description: "Browse drives, folders, shared items, and sites available to the account.",
162
+ },
163
+ {
164
+ title: "Create and update content",
165
+ description: "Work with OneDrive and SharePoint files through reviewed tools.",
166
+ },
167
+ {
168
+ title: "Manage sharing",
169
+ description: "Review and update sharing links and permissions.",
170
+ },
171
+ ],
172
+ permissionSummary:
173
+ "Microsoft asks for file and site access anywhere the connected account already has access.",
174
+ scopeLabels: {
175
+ "Files.ReadWrite.All": {
176
+ label: "Work with accessible files",
177
+ description: "Read, create, update, and organize files available to this account.",
178
+ },
179
+ "Sites.ReadWrite.All": {
180
+ label: "Work with accessible sites",
181
+ description: "Read and update files in SharePoint sites available to this account.",
182
+ },
183
+ },
184
+ },
185
+ };
@@ -0,0 +1,117 @@
1
+ import type { MCPServer } from "@openai/agents";
2
+
3
+ export const LOCAL_MCP_BRIDGE_CONTRACT_VERSION = 1 as const;
4
+
5
+ export type LocalMcpBridgeAuthority = "connection" | "host" | "none";
6
+ export type LocalMcpBridgeToolSurface = "static_reviewed";
7
+
8
+ export type LocalMcpBridgeDestination = Readonly<{
9
+ origin: string;
10
+ pathPrefix: string;
11
+ }>;
12
+
13
+ /**
14
+ * Secret-free description of an in-process provider-to-MCP adapter.
15
+ *
16
+ * This is observability and registration metadata, not authorization. The
17
+ * adapter must still revalidate its named authority before each physical
18
+ * provider request and keep credentials outside tool results and schemas.
19
+ */
20
+ export type LocalMcpBridgeDescriptor = Readonly<{
21
+ contractVersion: typeof LOCAL_MCP_BRIDGE_CONTRACT_VERSION;
22
+ adapterId: string;
23
+ providerId: string;
24
+ catalogIdentity: string;
25
+ transport: "in_process";
26
+ authority: LocalMcpBridgeAuthority;
27
+ toolSurface: LocalMcpBridgeToolSurface;
28
+ mutationReplay: "safe_reads_only";
29
+ destinations: readonly LocalMcpBridgeDestination[];
30
+ }>;
31
+
32
+ export interface LocalMcpBridgeServer extends MCPServer {
33
+ readonly bridge: LocalMcpBridgeDescriptor;
34
+ }
35
+
36
+ export interface LocalMcpBridgeAdapter<TConfig, TContext> {
37
+ readonly adapterId: string;
38
+ matches(config: TConfig): boolean;
39
+ create(config: TConfig, context: TContext): LocalMcpBridgeServer;
40
+ }
41
+
42
+ export function defineLocalMcpBridgeDescriptor(
43
+ input: Omit<LocalMcpBridgeDescriptor, "contractVersion" | "transport">,
44
+ ): LocalMcpBridgeDescriptor {
45
+ const adapterId = boundedIdentity(input.adapterId, "adapterId");
46
+ const providerId = boundedIdentity(input.providerId, "providerId");
47
+ const catalogIdentity = boundedIdentity(input.catalogIdentity, "catalogIdentity", 512);
48
+ if (input.destinations.length === 0 || input.destinations.length > 32) {
49
+ throw new Error("Local MCP bridge must declare 1-32 provider destinations");
50
+ }
51
+ const destinations = input.destinations.map((destination) => {
52
+ const url = new URL(destination.origin);
53
+ if (url.protocol !== "https:" || url.origin !== destination.origin) {
54
+ throw new Error("Local MCP bridge destinations must be exact HTTPS origins");
55
+ }
56
+ if (
57
+ !destination.pathPrefix.startsWith("/") ||
58
+ destination.pathPrefix.includes("\\") ||
59
+ destination.pathPrefix.includes("?") ||
60
+ destination.pathPrefix.includes("#") ||
61
+ new URL(destination.pathPrefix, url.origin).pathname !== destination.pathPrefix
62
+ ) {
63
+ throw new Error("Local MCP bridge destination pathPrefix must be an absolute URL path");
64
+ }
65
+ return Object.freeze({ origin: url.origin, pathPrefix: destination.pathPrefix });
66
+ });
67
+ return Object.freeze({
68
+ contractVersion: LOCAL_MCP_BRIDGE_CONTRACT_VERSION,
69
+ adapterId,
70
+ providerId,
71
+ catalogIdentity,
72
+ transport: "in_process",
73
+ authority: input.authority,
74
+ toolSurface: input.toolSurface,
75
+ mutationReplay: input.mutationReplay,
76
+ destinations: Object.freeze(destinations),
77
+ });
78
+ }
79
+
80
+ export function isLocalMcpBridgeServer(server: MCPServer): server is LocalMcpBridgeServer {
81
+ const bridge = (server as Partial<LocalMcpBridgeServer>).bridge;
82
+ return (
83
+ bridge?.contractVersion === LOCAL_MCP_BRIDGE_CONTRACT_VERSION &&
84
+ bridge.transport === "in_process"
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Select exactly one adapter for a runtime catalog row. Ambiguous matches fail
90
+ * closed so adding a bridge cannot silently replace another provider route.
91
+ */
92
+ export function createLocalMcpBridgeFromAdapters<TConfig, TContext>(
93
+ adapters: readonly LocalMcpBridgeAdapter<TConfig, TContext>[],
94
+ config: TConfig,
95
+ context: TContext,
96
+ ): LocalMcpBridgeServer | null {
97
+ const matches = adapters.filter((adapter) => adapter.matches(config));
98
+ if (matches.length === 0) return null;
99
+ if (matches.length > 1) {
100
+ throw new Error(
101
+ `Multiple local MCP bridge adapters matched: ${matches.map((entry) => entry.adapterId).join(", ")}`,
102
+ );
103
+ }
104
+ const adapter = matches[0]!;
105
+ const server = adapter.create(config, context);
106
+ if (server.bridge.adapterId !== adapter.adapterId) {
107
+ throw new Error(`Local MCP bridge adapter ${adapter.adapterId} returned mismatched metadata`);
108
+ }
109
+ return server;
110
+ }
111
+
112
+ function boundedIdentity(value: string, name: string, max = 128): string {
113
+ if (value.length === 0 || value.length > max || /[\u0000-\u001f\u007f]/u.test(value)) {
114
+ throw new Error(`Local MCP bridge ${name} is invalid`);
115
+ }
116
+ return value;
117
+ }
package/src/openapi.ts CHANGED
@@ -426,6 +426,22 @@ async function sendOpenApiRequest(
426
426
  const headers = buildOperationHeaders(binding, args);
427
427
  const body = buildOperationBody(binding, args, headers);
428
428
  if (credential) applyCredentialPlacements(url, headers, credential);
429
+ if (credential?.authorizeProviderRequest) {
430
+ let authorized = false;
431
+ try {
432
+ authorized = await credential.authorizeProviderRequest();
433
+ } catch {
434
+ authorized = false;
435
+ }
436
+ if (!authorized) {
437
+ throw new IntegrationInvocationError(
438
+ "authorization_rejected",
439
+ "The connected account is no longer authorized for this operation",
440
+ "not_started",
441
+ false,
442
+ );
443
+ }
444
+ }
429
445
  return await fetchWithDeadline(
430
446
  options.transport,
431
447
  url,
package/src/types.ts CHANGED
@@ -37,6 +37,8 @@ export interface IntegrationCredentialAudience {
37
37
  export interface ResolvedIntegrationCredential {
38
38
  readonly audience: IntegrationCredentialAudience;
39
39
  readonly placements: readonly IntegrationCredentialPlacement[];
40
+ /** Exact accepted-attempt fence invoked immediately before one HTTP request. */
41
+ readonly authorizeProviderRequest?: () => Promise<boolean>;
40
42
  readonly expiresAt?: string;
41
43
  readonly scope?: readonly string[];
42
44
  }