@mcp-ts/sdk 1.3.9 → 1.4.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.
Files changed (47) hide show
  1. package/README.md +0 -1
  2. package/dist/adapters/langchain-adapter.js.map +1 -1
  3. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  4. package/dist/client/index.d.mts +3 -189
  5. package/dist/client/index.d.ts +3 -189
  6. package/dist/client/index.js +218 -54
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/client/index.mjs +215 -55
  9. package/dist/client/index.mjs.map +1 -1
  10. package/dist/client/react.d.mts +29 -40
  11. package/dist/client/react.d.ts +29 -40
  12. package/dist/client/react.js +492 -147
  13. package/dist/client/react.js.map +1 -1
  14. package/dist/client/react.mjs +490 -149
  15. package/dist/client/react.mjs.map +1 -1
  16. package/dist/client/vue.d.mts +3 -2
  17. package/dist/client/vue.d.ts +3 -2
  18. package/dist/client/vue.js +239 -63
  19. package/dist/client/vue.js.map +1 -1
  20. package/dist/client/vue.mjs +236 -64
  21. package/dist/client/vue.mjs.map +1 -1
  22. package/dist/index-CQr9q0bF.d.mts +295 -0
  23. package/dist/index-nE_7Io0I.d.ts +295 -0
  24. package/dist/index.d.mts +2 -1
  25. package/dist/index.d.ts +2 -1
  26. package/dist/index.js +315 -64
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +303 -65
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/server/index.js +93 -10
  31. package/dist/server/index.js.map +1 -1
  32. package/dist/server/index.mjs +88 -10
  33. package/dist/server/index.mjs.map +1 -1
  34. package/package.json +13 -11
  35. package/src/adapters/langchain-adapter.ts +1 -1
  36. package/src/client/core/app-host.ts +252 -65
  37. package/src/client/core/constants.ts +30 -0
  38. package/src/client/index.ts +6 -1
  39. package/src/client/react/index.ts +6 -1
  40. package/src/client/react/use-app-host.ts +13 -19
  41. package/src/client/react/use-mcp-apps.tsx +297 -125
  42. package/src/client/react/use-mcp.ts +75 -36
  43. package/src/client/utils/app-host-utils.ts +62 -0
  44. package/src/client/vue/use-mcp.ts +23 -12
  45. package/src/server/mcp/oauth-client.ts +31 -8
  46. package/src/server/storage/crypto.ts +92 -0
  47. package/src/server/storage/supabase-backend.ts +7 -6
@@ -0,0 +1,62 @@
1
+ import { APP_HOST_DEFAULTS, SANDBOX_PROXY_READY_METHOD } from '../core/constants.js';
2
+
3
+ const DEFAULT_SANDBOX_TIMEOUT_MS = APP_HOST_DEFAULTS.SANDBOX_TIMEOUT_MS;
4
+
5
+ export async function setupSandboxProxyIframe(
6
+ iframe: HTMLIFrameElement,
7
+ sandboxProxyUrl: URL
8
+ ): Promise<{
9
+ onReady: Promise<void>;
10
+ }> {
11
+ iframe.style.width = '100%';
12
+ iframe.style.height = '100%';
13
+ iframe.style.border = 'none';
14
+ iframe.style.backgroundColor = 'transparent';
15
+ iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads');
16
+
17
+ const onReady = new Promise<void>((resolve, reject) => {
18
+ let settled = false;
19
+
20
+ const cleanup = () => {
21
+ window.removeEventListener('message', messageListener);
22
+ iframe.removeEventListener('error', errorListener);
23
+ };
24
+
25
+ const timeoutId = setTimeout(() => {
26
+ if (!settled) {
27
+ settled = true;
28
+ cleanup();
29
+ reject(new Error('Timed out waiting for sandbox proxy iframe to be ready'));
30
+ }
31
+ }, DEFAULT_SANDBOX_TIMEOUT_MS);
32
+
33
+ const messageListener = (event: MessageEvent) => {
34
+ if (event.source === iframe.contentWindow) {
35
+ if (event.data?.method === SANDBOX_PROXY_READY_METHOD) {
36
+ if (!settled) {
37
+ settled = true;
38
+ clearTimeout(timeoutId);
39
+ cleanup();
40
+ resolve();
41
+ }
42
+ }
43
+ }
44
+ };
45
+
46
+ const errorListener = () => {
47
+ if (!settled) {
48
+ settled = true;
49
+ clearTimeout(timeoutId);
50
+ cleanup();
51
+ reject(new Error('Failed to load sandbox proxy iframe'));
52
+ }
53
+ };
54
+
55
+ window.addEventListener('message', messageListener);
56
+ iframe.addEventListener('error', errorListener);
57
+ });
58
+
59
+ iframe.src = sandboxProxyUrl.href;
60
+
61
+ return { onReady };
62
+ }
@@ -285,22 +285,33 @@ export function useMcp(options: UseMcpOptions): McpClient {
285
285
  }
286
286
 
287
287
  case 'auth_required': {
288
- // Handle OAuth redirect
289
- if (event.authUrl) {
290
- onLog?.('info', `OAuth required - redirecting to ${event.authUrl}`, { authUrl: event.authUrl });
291
-
292
- // Suppress redirects/popups for background auto-restore on page load.
293
- if (!suppressAuthRedirectSessions.value.has(event.sessionId)) {
294
- if (onRedirect) {
295
- onRedirect(event.authUrl);
296
- } else if (typeof window !== 'undefined') {
297
- window.location.href = event.authUrl;
298
- }
288
+ const url = (event.authUrl || '').trim();
289
+ if (!url) {
290
+ onLog?.('error', 'OAuth required but authorization URL is missing', { sessionId: event.sessionId });
291
+ const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
292
+ if (index !== -1) {
293
+ connections.value[index] = {
294
+ ...connections.value[index],
295
+ state: 'FAILED',
296
+ error: 'OAuth authorization URL not available',
297
+ authUrl: undefined,
298
+ };
299
+ }
300
+ break;
301
+ }
302
+ onLog?.('info', `OAuth required - redirecting to ${url}`, { authUrl: url });
303
+
304
+ // Suppress redirects/popups for background auto-restore on page load.
305
+ if (!suppressAuthRedirectSessions.value.has(event.sessionId)) {
306
+ if (onRedirect) {
307
+ onRedirect(url);
308
+ } else if (typeof window !== 'undefined') {
309
+ window.location.href = url;
299
310
  }
300
311
  }
301
312
  const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
302
313
  if (index !== -1) {
303
- connections.value[index] = { ...connections.value[index], state: 'AUTHENTICATING', authUrl: event.authUrl };
314
+ connections.value[index] = { ...connections.value[index], state: 'AUTHENTICATING', authUrl: url };
304
315
  }
305
316
  break;
306
317
  }
@@ -51,12 +51,12 @@ export type TransportType = 'sse' | 'streamable_http';
51
51
  */
52
52
  import type { ClientCapabilities } from '@modelcontextprotocol/sdk/types.js';
53
53
 
54
- interface McpAppClientCapabilities extends ClientCapabilities {
54
+ interface McpAppClientCapabilities extends Omit<ClientCapabilities, 'extensions'> {
55
55
  extensions?: {
56
56
  'io.modelcontextprotocol/ui'?: {
57
57
  mimeTypes: string[];
58
58
  };
59
- [key: string]: unknown;
59
+ [key: string]: any;
60
60
  };
61
61
  }
62
62
 
@@ -516,17 +516,40 @@ export class MCPClient {
516
516
  error instanceof SDKUnauthorizedError ||
517
517
  (error instanceof Error && error.message.toLowerCase().includes('unauthorized'))
518
518
  ) {
519
+ /** Set when the SDK calls redirectToAuthorization on the OAuth provider */
520
+ let authUrl = '';
521
+ if (this.oauthProvider) {
522
+ authUrl = (this.oauthProvider.authUrl || '').trim();
523
+ }
524
+
525
+ /**
526
+ * 401 without a usable URL means metadata/DCR failed or the server never started
527
+ * an interactive OAuth flow — not recoverable as "pending OAuth".
528
+ */
529
+ if (!authUrl) {
530
+ const detail =
531
+ error instanceof Error && error.message.trim().length > 0
532
+ ? error.message.trim()
533
+ : 'Unauthorized';
534
+ const message =
535
+ detail.toLowerCase() === 'unauthorized'
536
+ ? 'OAuth authorization URL not available'
537
+ : `OAuth authorization URL not available: ${detail}`;
538
+ this.emitError(message, 'auth');
539
+ this.emitStateChange('FAILED');
540
+ try {
541
+ await storage.removeSession(this.identity, this.sessionId);
542
+ } catch {
543
+ // best-effort cleanup
544
+ }
545
+ throw new Error(message);
546
+ }
547
+
519
548
  this.emitStateChange('AUTHENTICATING');
520
549
  // Save session with 10min TTL for OAuth pending state
521
550
  console.log(`[MCPClient] Saving session ${this.sessionId} with 10min TTL (OAuth pending)`);
522
551
  await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1000), false);
523
552
 
524
- /** Get OAuth authorization URL if available */
525
- let authUrl = '';
526
- if (this.oauthProvider) {
527
- authUrl = this.oauthProvider.authUrl || '';
528
- }
529
-
530
553
  if (this.serverId) {
531
554
  this._onConnectionEvent.fire({
532
555
  type: 'auth_required',
@@ -0,0 +1,92 @@
1
+ import { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';
2
+
3
+ const ALGORITHM = 'aes-256-gcm';
4
+ const IV_LENGTH = 12;
5
+ const ENCRYPTION_PREFIX = 'enc:1:';
6
+
7
+ let warningLogged = false;
8
+
9
+ function getKey(): Buffer | null {
10
+ const keyString = process.env.STORAGE_ENCRYPTION_KEY;
11
+ if (!keyString) return null;
12
+
13
+ // Ensure key is 32 bytes (256 bits)
14
+ if (keyString.length === 64) {
15
+ return Buffer.from(keyString, 'hex');
16
+ } else {
17
+ const keyBuffer = Buffer.alloc(32);
18
+ keyBuffer.write(keyString, 0, 32, 'utf-8');
19
+ return keyBuffer;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Encrypts an object into a secure string.
25
+ * Falls back to returning the original object if the encryption key is missing or encryption fails.
26
+ */
27
+ export function encryptObject(data: any): any {
28
+ if (data === undefined || data === null) return data;
29
+
30
+ const key = getKey();
31
+ if (!key) {
32
+ if (!warningLogged) {
33
+ console.warn('[mcp-ts][Storage] WARNING: STORAGE_ENCRYPTION_KEY is not set. Saving sensitive data in plain-text.');
34
+ warningLogged = true;
35
+ }
36
+ return data; // Fallback to plain-text
37
+ }
38
+
39
+ try {
40
+ const text = JSON.stringify(data);
41
+ const iv = randomBytes(IV_LENGTH);
42
+ const cipher = createCipheriv(ALGORITHM, key, iv);
43
+
44
+ let encrypted = cipher.update(text, 'utf-8', 'hex');
45
+ encrypted += cipher.final('hex');
46
+ const authTag = cipher.getAuthTag().toString('hex');
47
+
48
+ return `${ENCRYPTION_PREFIX}${iv.toString('hex')}:${authTag}:${encrypted}`;
49
+ } catch (e) {
50
+ console.error('[mcp-ts][Storage] Encryption failed, falling back to plain-text.', e);
51
+ return data;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Decrypts a secure string back into an object.
57
+ * Returns the original data if it is unencrypted or if decryption fails.
58
+ */
59
+ export function decryptObject(data: any): any {
60
+ if (data === undefined || data === null) return data;
61
+ if (typeof data !== 'string' || !data.startsWith(ENCRYPTION_PREFIX)) {
62
+ return data; // Already unencrypted or old plain-text data
63
+ }
64
+
65
+ const key = getKey();
66
+ if (!key) {
67
+ console.warn('[mcp-ts][Storage] WARNING: Found encrypted data but STORAGE_ENCRYPTION_KEY is missing. Returning raw encrypted string.');
68
+ return data;
69
+ }
70
+
71
+ try {
72
+ const parts = data.split(':');
73
+ if (parts.length !== 5) {
74
+ return data;
75
+ }
76
+
77
+ const iv = Buffer.from(parts[2], 'hex');
78
+ const authTag = Buffer.from(parts[3], 'hex');
79
+ const encryptedText = parts[4];
80
+
81
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
82
+ decipher.setAuthTag(authTag);
83
+
84
+ let decrypted = decipher.update(encryptedText, 'hex', 'utf-8');
85
+ decrypted += decipher.final('utf-8');
86
+
87
+ return JSON.parse(decrypted);
88
+ } catch (e) {
89
+ console.error('[mcp-ts][Storage] Decryption failed.', e);
90
+ return data;
91
+ }
92
+ }
@@ -2,6 +2,7 @@ import type { SupabaseClient } from '@supabase/supabase-js';
2
2
  import { StorageBackend, SessionData } from './types.js';
3
3
  import { SESSION_TTL_SECONDS } from '../../shared/constants.js';
4
4
  import { generateSessionId } from '../../shared/utils.js';
5
+ import { encryptObject, decryptObject } from './crypto.js';
5
6
 
6
7
  export class SupabaseStorageBackend implements StorageBackend {
7
8
  private readonly DEFAULT_TTL = SESSION_TTL_SECONDS;
@@ -43,10 +44,10 @@ export class SupabaseStorageBackend implements StorageBackend {
43
44
  callbackUrl: row.callback_url,
44
45
  createdAt: new Date(row.created_at).getTime(),
45
46
  identity: row.identity,
46
- headers: row.headers,
47
+ headers: decryptObject(row.headers),
47
48
  active: row.active,
48
49
  clientInformation: row.client_information,
49
- tokens: row.tokens,
50
+ tokens: decryptObject(row.tokens),
50
51
  codeVerifier: row.code_verifier,
51
52
  clientId: row.client_id,
52
53
  };
@@ -71,10 +72,10 @@ export class SupabaseStorageBackend implements StorageBackend {
71
72
  callback_url: session.callbackUrl,
72
73
  created_at: new Date(session.createdAt || Date.now()).toISOString(),
73
74
  identity: identity,
74
- headers: session.headers,
75
+ headers: encryptObject(session.headers),
75
76
  active: session.active ?? false,
76
77
  client_information: session.clientInformation,
77
- tokens: session.tokens,
78
+ tokens: encryptObject(session.tokens),
78
79
  code_verifier: session.codeVerifier,
79
80
  client_id: session.clientId,
80
81
  expires_at: expiresAt
@@ -105,9 +106,9 @@ export class SupabaseStorageBackend implements StorageBackend {
105
106
  if ('transportType' in data) updateData.transport_type = data.transportType;
106
107
  if ('callbackUrl' in data) updateData.callback_url = data.callbackUrl;
107
108
  if ('active' in data) updateData.active = data.active;
108
- if ('headers' in data) updateData.headers = data.headers;
109
+ if ('headers' in data) updateData.headers = encryptObject(data.headers);
109
110
  if ('clientInformation' in data) updateData.client_information = data.clientInformation;
110
- if ('tokens' in data) updateData.tokens = data.tokens;
111
+ if ('tokens' in data) updateData.tokens = encryptObject(data.tokens);
111
112
  if ('codeVerifier' in data) updateData.code_verifier = data.codeVerifier;
112
113
  if ('clientId' in data) updateData.client_id = data.clientId;
113
114