@mcp-ts/sdk 1.3.9 → 1.3.10
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 +0 -1
- package/dist/client/react.d.mts +9 -27
- package/dist/client/react.d.ts +9 -27
- package/dist/client/react.js +99 -73
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +100 -74
- package/dist/client/react.mjs.map +1 -1
- package/dist/index.js +78 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +73 -6
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.js +78 -6
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +73 -6
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client/react/index.ts +5 -1
- package/src/client/react/use-app-host.ts +5 -4
- package/src/client/react/use-mcp-apps.tsx +89 -112
- package/src/client/react/use-mcp.ts +52 -24
- package/src/server/storage/crypto.ts +92 -0
- package/src/server/storage/supabase-backend.ts +7 -6
package/package.json
CHANGED
|
@@ -10,7 +10,11 @@ export { useMcp, type UseMcpOptions, type McpClient, type McpConnection } from '
|
|
|
10
10
|
export { useAppHost } from './use-app-host.js';
|
|
11
11
|
|
|
12
12
|
// Simplified MCP Apps Hook - the main API
|
|
13
|
-
export {
|
|
13
|
+
export {
|
|
14
|
+
useMcpApps,
|
|
15
|
+
type McpAppRendererProps,
|
|
16
|
+
type McpAppMetadata,
|
|
17
|
+
} from './use-mcp-apps.js';
|
|
14
18
|
|
|
15
19
|
// Re-export shared types and client from main entry
|
|
16
20
|
export * from '../index.js';
|
|
@@ -5,10 +5,11 @@ import { AppHost } from '../core/app-host';
|
|
|
5
5
|
/**
|
|
6
6
|
* Hook to host an MCP App in a React component
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
8
|
+
* Initialization is async but optimized for instant availability:
|
|
9
|
+
* - Constructor runs synchronously (sandbox + bridge handler setup)
|
|
10
|
+
* - Host is set in state immediately so launch() can be called right away
|
|
11
|
+
* - start() is a lightweight no-op reserved for future async pre-init work
|
|
12
|
+
* - The real async work (iframe load, bridge connect) happens inside launch()
|
|
12
13
|
*
|
|
13
14
|
* @param client - Connected SSEClient instance
|
|
14
15
|
* @param iframeRef - Reference to the iframe element
|
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
* Provides utilities for rendering interactive UI components from MCP servers.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, {
|
|
7
|
+
import React, {
|
|
8
|
+
useState,
|
|
9
|
+
useEffect,
|
|
10
|
+
useCallback,
|
|
11
|
+
useRef,
|
|
12
|
+
memo,
|
|
13
|
+
useMemo,
|
|
14
|
+
type MutableRefObject,
|
|
15
|
+
} from 'react';
|
|
8
16
|
import { useAppHost } from './use-app-host.js';
|
|
9
17
|
import type { SSEClient } from '../core/sse-client.js';
|
|
10
18
|
|
|
@@ -33,8 +41,8 @@ export interface McpAppMetadata {
|
|
|
33
41
|
sessionId: string;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
/** Props for {@link useMcpApps}'s `McpAppRenderer` (client is supplied via the hook). */
|
|
45
|
+
export interface McpAppRendererProps {
|
|
38
46
|
name: string;
|
|
39
47
|
input?: Record<string, unknown>;
|
|
40
48
|
result?: unknown;
|
|
@@ -43,90 +51,68 @@ interface McpAppRendererProps {
|
|
|
43
51
|
className?: string;
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
type McpAppViewProps = McpAppRendererProps & {
|
|
55
|
+
/**
|
|
56
|
+
* Ref avoids tying `McpAppRenderer` identity to `mcpClient`: when `connections` updates, `useMcp()` still
|
|
57
|
+
* returns a new object (correct for `useEffect` deps), but the iframe must not remount.
|
|
58
|
+
*/
|
|
59
|
+
clientRef: MutableRefObject<McpClient | null>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Renders one MCP App in a sandboxed iframe; reads the latest client from `clientRef` each render. */
|
|
63
|
+
const McpAppView = memo(function McpAppView({
|
|
64
|
+
clientRef,
|
|
52
65
|
name,
|
|
53
66
|
input,
|
|
54
67
|
result,
|
|
55
68
|
status,
|
|
56
69
|
className,
|
|
57
|
-
}:
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const extractedName = extractToolName(name);
|
|
62
|
-
|
|
63
|
-
for (const conn of mcpClient.connections) {
|
|
64
|
-
for (const tool of conn.tools) {
|
|
65
|
-
const candidateName = extractToolName(tool.name);
|
|
66
|
-
const resourceUri =
|
|
67
|
-
tool.mcpApp?.resourceUri ??
|
|
68
|
-
tool._meta?.ui?.resourceUri ??
|
|
69
|
-
tool._meta?.['ui/resourceUri'];
|
|
70
|
-
|
|
71
|
-
if (resourceUri && candidateName === extractedName) {
|
|
72
|
-
return {
|
|
73
|
-
toolName: candidateName,
|
|
74
|
-
resourceUri,
|
|
75
|
-
sessionId: conn.sessionId,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return undefined;
|
|
82
|
-
}, [mcpClient, name]);
|
|
83
|
-
|
|
84
|
-
const metadata = getAppMetadata();
|
|
70
|
+
}: McpAppViewProps) {
|
|
71
|
+
const mcpClient = clientRef.current;
|
|
72
|
+
const metadata = getMcpAppMetadata(mcpClient, name);
|
|
85
73
|
const sseClient = mcpClient?.sseClient ?? null;
|
|
74
|
+
const resourceUri = metadata?.resourceUri;
|
|
75
|
+
const appSessionId = metadata?.sessionId;
|
|
86
76
|
|
|
87
|
-
if (!metadata || !sseClient) {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
77
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
91
78
|
const { host, error: hostError } = useAppHost(sseClient as SSEClient, iframeRef);
|
|
92
79
|
const [isLaunched, setIsLaunched] = useState(false);
|
|
93
80
|
const [error, setError] = useState<Error | null>(null);
|
|
94
81
|
|
|
95
|
-
// Track which data has been sent to prevent duplicates
|
|
96
82
|
const sentInputRef = useRef(false);
|
|
97
83
|
const sentResultRef = useRef(false);
|
|
98
84
|
const lastInputRef = useRef(input);
|
|
99
85
|
const lastResultRef = useRef(result);
|
|
100
86
|
const lastStatusRef = useRef(status);
|
|
101
87
|
|
|
102
|
-
// Launch the app when host is ready
|
|
103
88
|
useEffect(() => {
|
|
104
|
-
|
|
89
|
+
setIsLaunched(false);
|
|
90
|
+
setError(null);
|
|
91
|
+
}, [resourceUri, appSessionId]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!host || !resourceUri || !appSessionId) return;
|
|
105
95
|
|
|
106
96
|
host
|
|
107
|
-
.launch(
|
|
97
|
+
.launch(resourceUri, appSessionId)
|
|
108
98
|
.then(() => setIsLaunched(true))
|
|
109
99
|
.catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
|
|
110
|
-
}, [host,
|
|
100
|
+
}, [host, resourceUri, appSessionId]);
|
|
111
101
|
|
|
112
|
-
// Send tool input when available or when it changes
|
|
113
102
|
useEffect(() => {
|
|
114
|
-
if (!host || !isLaunched || !input) return;
|
|
115
|
-
|
|
116
|
-
// Send if never sent, or if input changed
|
|
103
|
+
if (!host || !isLaunched || !resourceUri || !appSessionId || !input) return;
|
|
104
|
+
|
|
117
105
|
if (!sentInputRef.current || JSON.stringify(input) !== JSON.stringify(lastInputRef.current)) {
|
|
118
106
|
sentInputRef.current = true;
|
|
119
107
|
lastInputRef.current = input;
|
|
120
108
|
host.sendToolInput(input);
|
|
121
109
|
}
|
|
122
|
-
}, [host, isLaunched, input]);
|
|
110
|
+
}, [host, isLaunched, input, resourceUri, appSessionId, name]);
|
|
123
111
|
|
|
124
|
-
// Send tool result when complete or when it changes
|
|
125
112
|
useEffect(() => {
|
|
126
|
-
if (!host || !isLaunched || result === undefined) return;
|
|
113
|
+
if (!host || !isLaunched || !resourceUri || !appSessionId || result === undefined) return;
|
|
127
114
|
if (status !== 'complete') return;
|
|
128
115
|
|
|
129
|
-
// Send if never sent, or if result changed
|
|
130
116
|
if (!sentResultRef.current || JSON.stringify(result) !== JSON.stringify(lastResultRef.current)) {
|
|
131
117
|
sentResultRef.current = true;
|
|
132
118
|
lastResultRef.current = result;
|
|
@@ -136,9 +122,8 @@ const McpAppRenderer = memo(function McpAppRenderer({
|
|
|
136
122
|
: result;
|
|
137
123
|
host.sendToolResult(formattedResult);
|
|
138
124
|
}
|
|
139
|
-
}, [host, isLaunched, result, status]);
|
|
125
|
+
}, [host, isLaunched, result, status, resourceUri, appSessionId, name]);
|
|
140
126
|
|
|
141
|
-
// Reset sent flags when tool status resets to executing (new tool call)
|
|
142
127
|
useEffect(() => {
|
|
143
128
|
if (status === 'executing' && lastStatusRef.current !== 'executing') {
|
|
144
129
|
sentInputRef.current = false;
|
|
@@ -147,7 +132,10 @@ const McpAppRenderer = memo(function McpAppRenderer({
|
|
|
147
132
|
lastStatusRef.current = status;
|
|
148
133
|
}, [status]);
|
|
149
134
|
|
|
150
|
-
|
|
135
|
+
if (!metadata || !sseClient) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
151
139
|
const displayError = error || hostError;
|
|
152
140
|
if (displayError) {
|
|
153
141
|
return (
|
|
@@ -176,72 +164,61 @@ const McpAppRenderer = memo(function McpAppRenderer({
|
|
|
176
164
|
});
|
|
177
165
|
|
|
178
166
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* @param mcpClient - The MCP client from useMcp() or context
|
|
182
|
-
* @returns Object with getAppMetadata function and McpAppRenderer component
|
|
183
|
-
*
|
|
184
|
-
* @example
|
|
185
|
-
* ```tsx
|
|
186
|
-
* function ToolRenderer(props) {
|
|
187
|
-
* const { getAppMetadata, McpAppRenderer } = useMcpApps(mcpClient);
|
|
188
|
-
* const metadata = getAppMetadata(props.name);
|
|
167
|
+
* Helpers scoped to one `mcpClient`. Pass the client here once; `McpAppRenderer` only needs per-tool props (`name`, `input`, `result`, `status`).
|
|
189
168
|
*
|
|
190
|
-
*
|
|
191
|
-
* return (
|
|
192
|
-
* <McpAppRenderer
|
|
193
|
-
* metadata={metadata}
|
|
194
|
-
* input={props.args}
|
|
195
|
-
* result={props.result}
|
|
196
|
-
* status={props.status}
|
|
197
|
-
* />
|
|
198
|
-
* );
|
|
199
|
-
* }
|
|
200
|
-
* ```
|
|
169
|
+
* @param mcpClient - From `useMcp()` or context (for example `useMcpContext()`).
|
|
201
170
|
*/
|
|
202
171
|
export function useMcpApps(mcpClient: McpClient | null) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
*/
|
|
207
|
-
const getAppMetadata = useCallback(
|
|
208
|
-
(toolName: string): McpAppMetadata | undefined => {
|
|
209
|
-
if (!mcpClient) return undefined;
|
|
210
|
-
|
|
211
|
-
const extractedName = extractToolName(toolName);
|
|
212
|
-
|
|
213
|
-
for (const conn of mcpClient.connections) {
|
|
214
|
-
for (const tool of conn.tools) {
|
|
215
|
-
const candidateName = extractToolName(tool.name);
|
|
216
|
-
// Check both locations: direct mcpApp or _meta.ui
|
|
217
|
-
const resourceUri =
|
|
218
|
-
tool.mcpApp?.resourceUri ??
|
|
219
|
-
tool._meta?.ui?.resourceUri ??
|
|
220
|
-
tool._meta?.['ui/resourceUri'];
|
|
221
|
-
|
|
222
|
-
if (resourceUri && candidateName === extractedName) {
|
|
223
|
-
return {
|
|
224
|
-
toolName: candidateName,
|
|
225
|
-
resourceUri,
|
|
226
|
-
sessionId: conn.sessionId,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
172
|
+
// Stable `McpAppRenderer` type: parent re-renders and `connections` updates must not remount the iframe.
|
|
173
|
+
const clientRef = useRef(mcpClient);
|
|
174
|
+
clientRef.current = mcpClient;
|
|
231
175
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
[
|
|
176
|
+
const getAppMetadata = useCallback(
|
|
177
|
+
(toolName: string) => getMcpAppMetadata(clientRef.current, toolName),
|
|
178
|
+
[]
|
|
235
179
|
);
|
|
236
180
|
|
|
181
|
+
const McpAppRenderer = useMemo(() => {
|
|
182
|
+
const Renderer = memo(function McpAppRenderer(props: McpAppRendererProps) {
|
|
183
|
+
return <McpAppView clientRef={clientRef} {...props} />;
|
|
184
|
+
});
|
|
185
|
+
Renderer.displayName = 'McpAppRenderer';
|
|
186
|
+
return Renderer;
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
237
189
|
return { getAppMetadata, McpAppRenderer };
|
|
238
190
|
}
|
|
239
191
|
|
|
240
|
-
/**
|
|
241
|
-
* Extract the base tool name, removing any prefixes
|
|
242
|
-
*/
|
|
243
192
|
function extractToolName(fullName: string): string {
|
|
244
|
-
// Handle patterns like "tool_abc123_get-time" -> "get-time"
|
|
245
193
|
const match = fullName.match(/(?:tool_[^_]+_)?(.+)$/);
|
|
246
194
|
return match?.[1] || fullName;
|
|
247
195
|
}
|
|
196
|
+
|
|
197
|
+
function getMcpAppMetadata(
|
|
198
|
+
mcpClient: McpClient | null,
|
|
199
|
+
toolName: string
|
|
200
|
+
): McpAppMetadata | undefined {
|
|
201
|
+
if (!mcpClient) return undefined;
|
|
202
|
+
|
|
203
|
+
const extractedName = extractToolName(toolName);
|
|
204
|
+
|
|
205
|
+
for (const conn of mcpClient.connections) {
|
|
206
|
+
for (const tool of conn.tools) {
|
|
207
|
+
const candidateName = extractToolName(tool.name);
|
|
208
|
+
const resourceUri =
|
|
209
|
+
tool.mcpApp?.resourceUri ??
|
|
210
|
+
tool._meta?.ui?.resourceUri ??
|
|
211
|
+
tool._meta?.['ui/resourceUri'];
|
|
212
|
+
|
|
213
|
+
if (resourceUri && candidateName === extractedName) {
|
|
214
|
+
return {
|
|
215
|
+
toolName: candidateName,
|
|
216
|
+
resourceUri,
|
|
217
|
+
sessionId: conn.sessionId,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Based on Cloudflare's agents pattern
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
7
|
+
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
|
|
8
8
|
import { SSEClient, type SSEClientOptions } from '../core/sse-client';
|
|
9
9
|
import type { McpConnectionEvent, McpConnectionState } from '../../shared/events';
|
|
10
10
|
import type {
|
|
@@ -227,6 +227,8 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
227
227
|
'disconnected'
|
|
228
228
|
);
|
|
229
229
|
const [isInitializing, setIsInitializing] = useState(false);
|
|
230
|
+
/** Mirrored from `clientRef` so the public `McpClient` object can be memoized when the instance is ready. */
|
|
231
|
+
const [sseClient, setSseClient] = useState<SSEClient | null>(null);
|
|
230
232
|
|
|
231
233
|
/**
|
|
232
234
|
* Initialize SSE client
|
|
@@ -258,6 +260,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
258
260
|
|
|
259
261
|
const client = new SSEClient(clientOptions);
|
|
260
262
|
clientRef.current = client;
|
|
263
|
+
setSseClient(client);
|
|
261
264
|
|
|
262
265
|
if (autoConnect) {
|
|
263
266
|
client.connect();
|
|
@@ -615,27 +618,52 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
615
618
|
[getConnection]
|
|
616
619
|
);
|
|
617
620
|
|
|
618
|
-
return
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
621
|
+
return useMemo(
|
|
622
|
+
() => ({
|
|
623
|
+
connections,
|
|
624
|
+
status,
|
|
625
|
+
isInitializing,
|
|
626
|
+
connect,
|
|
627
|
+
disconnect,
|
|
628
|
+
getConnection,
|
|
629
|
+
getConnectionByServerId,
|
|
630
|
+
isServerConnected,
|
|
631
|
+
getTools,
|
|
632
|
+
refresh,
|
|
633
|
+
connectSSE,
|
|
634
|
+
disconnectSSE,
|
|
635
|
+
finishAuth,
|
|
636
|
+
resumeAuth,
|
|
637
|
+
callTool,
|
|
638
|
+
listTools,
|
|
639
|
+
listPrompts,
|
|
640
|
+
getPrompt,
|
|
641
|
+
listResources,
|
|
642
|
+
readResource,
|
|
643
|
+
sseClient,
|
|
644
|
+
}),
|
|
645
|
+
[
|
|
646
|
+
connections,
|
|
647
|
+
status,
|
|
648
|
+
isInitializing,
|
|
649
|
+
connect,
|
|
650
|
+
disconnect,
|
|
651
|
+
getConnection,
|
|
652
|
+
getConnectionByServerId,
|
|
653
|
+
isServerConnected,
|
|
654
|
+
getTools,
|
|
655
|
+
refresh,
|
|
656
|
+
connectSSE,
|
|
657
|
+
disconnectSSE,
|
|
658
|
+
finishAuth,
|
|
659
|
+
resumeAuth,
|
|
660
|
+
callTool,
|
|
661
|
+
listTools,
|
|
662
|
+
listPrompts,
|
|
663
|
+
getPrompt,
|
|
664
|
+
listResources,
|
|
665
|
+
readResource,
|
|
666
|
+
sseClient,
|
|
667
|
+
]
|
|
668
|
+
);
|
|
641
669
|
}
|
|
@@ -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
|
|