@ai-sdk/mcp 2.0.12 → 2.0.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/mcp",
3
- "version": "2.0.12",
3
+ "version": "2.0.14",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "pkce-challenge": "^5.0.1",
35
35
  "@ai-sdk/provider": "4.0.3",
36
- "@ai-sdk/provider-utils": "5.0.9"
36
+ "@ai-sdk/provider-utils": "5.0.10"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "22.19.19",
package/src/index.ts CHANGED
@@ -22,6 +22,10 @@ export {
22
22
  type MCPAppResourceCSP,
23
23
  type MCPAppResourceMeta,
24
24
  } from './tool/mcp-apps';
25
+ export {
26
+ fingerprintMCPAppResource,
27
+ detectMCPAppResourceDrift,
28
+ } from './tool/mcp-app-fingerprint';
25
29
  export { ElicitationRequestSchema, ElicitResultSchema } from './tool/types';
26
30
  export type {
27
31
  CallToolResult,
@@ -0,0 +1,70 @@
1
+ import { convertUint8ArrayToBase64 } from '@ai-sdk/provider-utils';
2
+ import type { MCPAppResource } from './mcp-apps';
3
+
4
+ const encoder = new TextEncoder();
5
+
6
+ // canonicalJSON/toBase64url/the digest below mirror
7
+ // packages/ai/src/util/canonical-hash.ts, which `@ai-sdk/mcp` can't import
8
+ // (wrong dependency direction, not exported). Keep them identical; if a third
9
+ // consumer appears, hoist into @ai-sdk/provider-utils instead.
10
+
11
+ /**
12
+ * Deterministic JSON serialization: object keys are sorted so two
13
+ * structurally-equal values always produce the same string regardless of key
14
+ * insertion order. Used as the stable input to content hashing.
15
+ */
16
+ function canonicalJSON(value: unknown): string {
17
+ if (value == null || typeof value !== 'object') {
18
+ return JSON.stringify(value ?? null);
19
+ }
20
+ if (Array.isArray(value)) {
21
+ return `[${value.map(canonicalJSON).join(',')}]`;
22
+ }
23
+ const record = value as Record<string, unknown>;
24
+ const entries = Object.keys(record)
25
+ .sort()
26
+ .map(k => `${JSON.stringify(k)}:${canonicalJSON(record[k])}`);
27
+ return `{${entries.join(',')}}`;
28
+ }
29
+
30
+ function toBase64url(bytes: Uint8Array): string {
31
+ return convertUint8ArrayToBase64(bytes)
32
+ .replace(/\+/g, '-')
33
+ .replace(/\//g, '_')
34
+ .replace(/=+$/g, '');
35
+ }
36
+
37
+ /**
38
+ * Stable SHA-256 digest (base64url) over an MCP App resource's `html`, `csp`,
39
+ * and `permissions`.
40
+ *
41
+ * Capture a baseline on first load and compare later reads with
42
+ * {@link detectMCPAppResourceDrift} to detect a changed resource. Baseline
43
+ * storage and the response are the host's concern, mirroring `fingerprintTools`.
44
+ */
45
+ export async function fingerprintMCPAppResource(
46
+ resource: MCPAppResource,
47
+ ): Promise<string> {
48
+ const digest = await crypto.subtle.digest(
49
+ 'SHA-256',
50
+ encoder.encode(
51
+ canonicalJSON({
52
+ html: resource.html,
53
+ csp: resource.meta?.csp ?? null,
54
+ permissions: resource.meta?.permissions ?? null,
55
+ }),
56
+ ),
57
+ );
58
+ return toBase64url(new Uint8Array(digest));
59
+ }
60
+
61
+ /**
62
+ * Compares two fingerprints from {@link fingerprintMCPAppResource}. Returns
63
+ * `true` when the current resource differs from its baseline.
64
+ */
65
+ export function detectMCPAppResourceDrift(
66
+ current: string,
67
+ baseline: string,
68
+ ): boolean {
69
+ return current !== baseline;
70
+ }
@@ -1,5 +1,6 @@
1
1
  import { isJSONObject, type JSONObject } from '@ai-sdk/provider';
2
2
  import { convertBase64ToUint8Array } from '@ai-sdk/provider-utils';
3
+ import * as z4 from 'zod/v4';
3
4
  import type { MCPClient } from './mcp-client';
4
5
  import type {
5
6
  ClientCapabilities,
@@ -87,12 +88,42 @@ function getToolUiMeta(meta?: ToolMeta): JSONObject | undefined {
87
88
  return isJSONObject(uiMeta) ? uiMeta : undefined;
88
89
  }
89
90
 
91
+ /**
92
+ * Filters a string array to its string elements, dropping the field entirely
93
+ * (rather than failing the parse) if the value is not an array.
94
+ */
95
+ const optionalStringArray = z4
96
+ .array(z4.unknown())
97
+ .transform(items => items.filter((v): v is string => typeof v === 'string'))
98
+ .optional()
99
+ .catch(undefined);
100
+
101
+ const MCPAppResourceCSPSchema = z4.looseObject({
102
+ connectDomains: optionalStringArray,
103
+ resourceDomains: optionalStringArray,
104
+ frameDomains: optionalStringArray,
105
+ });
106
+
107
+ /**
108
+ * Runtime schema for `_meta.ui` on an MCP App resource. Each known field is
109
+ * validated; a malformed field is dropped (`.catch(undefined)`) rather than
110
+ * failing the whole parse, and unknown keys pass through for forward-compat.
111
+ */
112
+ const MCPAppResourceMetaSchema = z4.looseObject({
113
+ prefersBorder: z4.boolean().optional().catch(undefined),
114
+ csp: MCPAppResourceCSPSchema.optional().catch(undefined),
115
+ permissions: z4.record(z4.string(), z4.unknown()).optional().catch(undefined),
116
+ });
117
+
90
118
  function getResourceUiMeta(meta: unknown): MCPAppResourceMeta | undefined {
91
119
  const resourceMeta = isJSONObject(meta) ? meta : undefined;
92
120
  const rawUiMeta = resourceMeta?.ui;
93
- const uiMeta = isJSONObject(rawUiMeta) ? rawUiMeta : undefined;
121
+ if (!isJSONObject(rawUiMeta)) {
122
+ return undefined;
123
+ }
94
124
 
95
- return uiMeta as MCPAppResourceMeta | undefined;
125
+ const parsed = MCPAppResourceMetaSchema.safeParse(rawUiMeta);
126
+ return parsed.success ? (parsed.data as MCPAppResourceMeta) : undefined;
96
127
  }
97
128
 
98
129
  function parseVisibility(value: unknown): MCPAppToolVisibility[] | undefined {