@mcp-ts/sdk 1.3.1 → 1.3.3
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 +371 -290
- package/dist/adapters/agui-adapter.d.mts +3 -3
- package/dist/adapters/agui-adapter.d.ts +3 -3
- package/dist/adapters/agui-middleware.d.mts +3 -3
- package/dist/adapters/agui-middleware.d.ts +3 -3
- package/dist/adapters/ai-adapter.d.mts +3 -3
- package/dist/adapters/ai-adapter.d.ts +3 -3
- package/dist/adapters/langchain-adapter.d.mts +3 -3
- package/dist/adapters/langchain-adapter.d.ts +3 -3
- package/dist/adapters/mastra-adapter.d.mts +3 -3
- package/dist/adapters/mastra-adapter.d.ts +3 -3
- package/dist/client/index.d.mts +10 -66
- package/dist/client/index.d.ts +10 -66
- package/dist/client/index.js +91 -173
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +91 -173
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +15 -5
- package/dist/client/react.d.ts +15 -5
- package/dist/client/react.js +130 -182
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +130 -182
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +27 -7
- package/dist/client/vue.d.ts +27 -7
- package/dist/client/vue.js +131 -182
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +131 -182
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{events-BgeztGYZ.d.mts → events-CK3N--3g.d.mts} +2 -0
- package/dist/{events-BgeztGYZ.d.ts → events-CK3N--3g.d.ts} +2 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +224 -258
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +224 -258
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-CxogNckF.d.mts → multi-session-client-DzjmT7FX.d.mts} +4 -10
- package/dist/{multi-session-client-cox_WXUj.d.ts → multi-session-client-FAFpUzZ4.d.ts} +4 -10
- package/dist/server/index.d.mts +18 -23
- package/dist/server/index.d.ts +18 -23
- package/dist/server/index.js +133 -85
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +133 -85
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +3 -3
- package/dist/shared/index.d.ts +3 -3
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{types-CLccx9wW.d.mts → types-CW6lghof.d.mts} +6 -0
- package/dist/{types-CLccx9wW.d.ts → types-CW6lghof.d.ts} +6 -0
- package/package.json +1 -1
- package/src/client/core/sse-client.ts +354 -493
- package/src/client/react/index.ts +16 -16
- package/src/client/react/use-mcp-apps.tsx +214 -214
- package/src/client/react/use-mcp.ts +84 -19
- package/src/client/vue/use-mcp.ts +119 -44
- package/src/server/handlers/nextjs-handler.ts +207 -217
- package/src/server/handlers/sse-handler.ts +14 -0
- package/src/server/mcp/oauth-client.ts +48 -46
- package/src/server/storage/types.ts +12 -5
- package/src/shared/events.ts +2 -0
- package/src/shared/types.ts +6 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP SDK - React Client
|
|
3
|
-
* Simple React hooks for MCP app rendering
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// Core MCP Hook
|
|
7
|
-
export { useMcp, type UseMcpOptions, type McpClient, type McpConnection } from './use-mcp.js';
|
|
8
|
-
|
|
9
|
-
// App Host (internal use)
|
|
10
|
-
export { useAppHost } from './use-app-host.js';
|
|
11
|
-
|
|
12
|
-
// Simplified MCP Apps Hook - the main API
|
|
13
|
-
export { useMcpApps } from './use-mcp-apps.js';
|
|
14
|
-
|
|
15
|
-
// Re-export shared types and client from main entry
|
|
16
|
-
export * from '../index.js';
|
|
1
|
+
/**
|
|
2
|
+
* MCP SDK - React Client
|
|
3
|
+
* Simple React hooks for MCP app rendering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Core MCP Hook
|
|
7
|
+
export { useMcp, type UseMcpOptions, type McpClient, type McpConnection } from './use-mcp.js';
|
|
8
|
+
|
|
9
|
+
// App Host (internal use)
|
|
10
|
+
export { useAppHost } from './use-app-host.js';
|
|
11
|
+
|
|
12
|
+
// Simplified MCP Apps Hook - the main API
|
|
13
|
+
export { useMcpApps } from './use-mcp-apps.js';
|
|
14
|
+
|
|
15
|
+
// Re-export shared types and client from main entry
|
|
16
|
+
export * from '../index.js';
|
|
@@ -1,214 +1,214 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Apps Hook
|
|
3
|
-
*
|
|
4
|
-
* Provides utilities for rendering interactive UI components from MCP servers.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import React, { useState, useEffect, useCallback, useRef, memo } from 'react';
|
|
8
|
-
import { useAppHost } from './use-app-host.js';
|
|
9
|
-
import type { SSEClient } from '../core/sse-client.js';
|
|
10
|
-
|
|
11
|
-
export interface McpClient {
|
|
12
|
-
connections: Array<{
|
|
13
|
-
sessionId: string;
|
|
14
|
-
tools: Array<{
|
|
15
|
-
name: string;
|
|
16
|
-
mcpApp?: {
|
|
17
|
-
resourceUri: string;
|
|
18
|
-
};
|
|
19
|
-
_meta?: {
|
|
20
|
-
ui?: {
|
|
21
|
-
resourceUri?: string;
|
|
22
|
-
};
|
|
23
|
-
'ui/resourceUri'?: string;
|
|
24
|
-
};
|
|
25
|
-
}>;
|
|
26
|
-
}>;
|
|
27
|
-
sseClient?: SSEClient | null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface McpAppMetadata {
|
|
31
|
-
toolName: string;
|
|
32
|
-
resourceUri: string;
|
|
33
|
-
sessionId: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface McpAppRendererProps {
|
|
37
|
-
metadata: McpAppMetadata;
|
|
38
|
-
input?: Record<string, unknown>;
|
|
39
|
-
result?: unknown;
|
|
40
|
-
status: 'executing' | 'inProgress' | 'complete' | 'idle';
|
|
41
|
-
sseClient?: SSEClient | null;
|
|
42
|
-
/** Custom CSS class for the container */
|
|
43
|
-
className?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Internal component that renders the MCP app in a sandboxed iframe
|
|
48
|
-
*/
|
|
49
|
-
const McpAppRenderer = memo(function McpAppRenderer({
|
|
50
|
-
metadata,
|
|
51
|
-
input,
|
|
52
|
-
result,
|
|
53
|
-
status,
|
|
54
|
-
sseClient,
|
|
55
|
-
className,
|
|
56
|
-
}: McpAppRendererProps) {
|
|
57
|
-
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
58
|
-
const { host, error: hostError } = useAppHost(sseClient as SSEClient, iframeRef);
|
|
59
|
-
const [isLaunched, setIsLaunched] = useState(false);
|
|
60
|
-
const [error, setError] = useState<Error | null>(null);
|
|
61
|
-
|
|
62
|
-
// Track which data has been sent to prevent duplicates
|
|
63
|
-
const sentInputRef = useRef(false);
|
|
64
|
-
const sentResultRef = useRef(false);
|
|
65
|
-
const lastInputRef = useRef(input);
|
|
66
|
-
const lastResultRef = useRef(result);
|
|
67
|
-
const lastStatusRef = useRef(status);
|
|
68
|
-
|
|
69
|
-
// Launch the app when host is ready
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
if (!host || !metadata.resourceUri || !metadata.sessionId) return;
|
|
72
|
-
|
|
73
|
-
host
|
|
74
|
-
.launch(metadata.resourceUri, metadata.sessionId)
|
|
75
|
-
.then(() => setIsLaunched(true))
|
|
76
|
-
.catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
|
|
77
|
-
}, [host, metadata.resourceUri, metadata.sessionId]);
|
|
78
|
-
|
|
79
|
-
// Send tool input when available or when it changes
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (!host || !isLaunched || !input) return;
|
|
82
|
-
|
|
83
|
-
// Send if never sent, or if input changed
|
|
84
|
-
if (!sentInputRef.current || JSON.stringify(input) !== JSON.stringify(lastInputRef.current)) {
|
|
85
|
-
sentInputRef.current = true;
|
|
86
|
-
lastInputRef.current = input;
|
|
87
|
-
host.sendToolInput(input);
|
|
88
|
-
}
|
|
89
|
-
}, [host, isLaunched, input]);
|
|
90
|
-
|
|
91
|
-
// Send tool result when complete or when it changes
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
if (!host || !isLaunched || result === undefined) return;
|
|
94
|
-
if (status !== 'complete') return;
|
|
95
|
-
|
|
96
|
-
// Send if never sent, or if result changed
|
|
97
|
-
if (!sentResultRef.current || JSON.stringify(result) !== JSON.stringify(lastResultRef.current)) {
|
|
98
|
-
sentResultRef.current = true;
|
|
99
|
-
lastResultRef.current = result;
|
|
100
|
-
const formattedResult =
|
|
101
|
-
typeof result === 'string'
|
|
102
|
-
? { content: [{ type: 'text', text: result }] }
|
|
103
|
-
: result;
|
|
104
|
-
host.sendToolResult(formattedResult);
|
|
105
|
-
}
|
|
106
|
-
}, [host, isLaunched, result, status]);
|
|
107
|
-
|
|
108
|
-
// Reset sent flags when tool status resets to executing (new tool call)
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
if (status === 'executing' && lastStatusRef.current !== 'executing') {
|
|
111
|
-
sentInputRef.current = false;
|
|
112
|
-
sentResultRef.current = false;
|
|
113
|
-
}
|
|
114
|
-
lastStatusRef.current = status;
|
|
115
|
-
}, [status]);
|
|
116
|
-
|
|
117
|
-
// Display errors
|
|
118
|
-
const displayError = error || hostError;
|
|
119
|
-
if (displayError) {
|
|
120
|
-
return (
|
|
121
|
-
<div className={`p-4 bg-red-900/20 border border-red-700 rounded text-red-200 ${className || ''}`}>
|
|
122
|
-
Error: {displayError.message || String(displayError)}
|
|
123
|
-
</div>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
<div className={`w-full border border-gray-700 rounded overflow-hidden bg-white min-h-96 my-2 relative ${className || ''}`}>
|
|
129
|
-
<iframe
|
|
130
|
-
ref={iframeRef}
|
|
131
|
-
sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads"
|
|
132
|
-
className="w-full h-full min-h-96"
|
|
133
|
-
style={{ height: 'auto' }}
|
|
134
|
-
title="MCP App"
|
|
135
|
-
/>
|
|
136
|
-
{!isLaunched && (
|
|
137
|
-
<div className="absolute inset-0 bg-gray-900/50 flex items-center justify-center pointer-events-none">
|
|
138
|
-
<div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
|
139
|
-
</div>
|
|
140
|
-
)}
|
|
141
|
-
</div>
|
|
142
|
-
);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Simple hook to get MCP app metadata
|
|
147
|
-
*
|
|
148
|
-
* @param mcpClient - The MCP client from useMcp() or context
|
|
149
|
-
* @returns Object with getAppMetadata function and McpAppRenderer component
|
|
150
|
-
*
|
|
151
|
-
* @example
|
|
152
|
-
* ```tsx
|
|
153
|
-
* function ToolRenderer(props) {
|
|
154
|
-
* const { getAppMetadata, McpAppRenderer } = useMcpApps(mcpClient);
|
|
155
|
-
* const metadata = getAppMetadata(props.name);
|
|
156
|
-
*
|
|
157
|
-
* if (!metadata) return null;
|
|
158
|
-
* return (
|
|
159
|
-
* <McpAppRenderer
|
|
160
|
-
* metadata={metadata}
|
|
161
|
-
* input={props.args}
|
|
162
|
-
* result={props.result}
|
|
163
|
-
* status={props.status}
|
|
164
|
-
* />
|
|
165
|
-
* );
|
|
166
|
-
* }
|
|
167
|
-
* ```
|
|
168
|
-
*/
|
|
169
|
-
export function useMcpApps(mcpClient: McpClient | null) {
|
|
170
|
-
/**
|
|
171
|
-
* Get MCP app metadata for a tool name
|
|
172
|
-
* This is fast and can be called on every render
|
|
173
|
-
*/
|
|
174
|
-
const getAppMetadata = useCallback(
|
|
175
|
-
(toolName: string): McpAppMetadata | undefined => {
|
|
176
|
-
if (!mcpClient) return undefined;
|
|
177
|
-
|
|
178
|
-
const extractedName = extractToolName(toolName);
|
|
179
|
-
|
|
180
|
-
for (const conn of mcpClient.connections) {
|
|
181
|
-
for (const tool of conn.tools) {
|
|
182
|
-
const candidateName = extractToolName(tool.name);
|
|
183
|
-
// Check both locations: direct mcpApp or _meta.ui
|
|
184
|
-
const resourceUri =
|
|
185
|
-
tool.mcpApp?.resourceUri ??
|
|
186
|
-
tool._meta?.ui?.resourceUri ??
|
|
187
|
-
tool._meta?.['ui/resourceUri'];
|
|
188
|
-
|
|
189
|
-
if (resourceUri && candidateName === extractedName) {
|
|
190
|
-
return {
|
|
191
|
-
toolName: candidateName,
|
|
192
|
-
resourceUri,
|
|
193
|
-
sessionId: conn.sessionId,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return undefined;
|
|
200
|
-
},
|
|
201
|
-
[mcpClient]
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
return { getAppMetadata, McpAppRenderer };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Extract the base tool name, removing any prefixes
|
|
209
|
-
*/
|
|
210
|
-
function extractToolName(fullName: string): string {
|
|
211
|
-
// Handle patterns like "tool_abc123_get-time" -> "get-time"
|
|
212
|
-
const match = fullName.match(/(?:tool_[^_]+_)?(.+)$/);
|
|
213
|
-
return match?.[1] || fullName;
|
|
214
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* MCP Apps Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for rendering interactive UI components from MCP servers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useEffect, useCallback, useRef, memo } from 'react';
|
|
8
|
+
import { useAppHost } from './use-app-host.js';
|
|
9
|
+
import type { SSEClient } from '../core/sse-client.js';
|
|
10
|
+
|
|
11
|
+
export interface McpClient {
|
|
12
|
+
connections: Array<{
|
|
13
|
+
sessionId: string;
|
|
14
|
+
tools: Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
mcpApp?: {
|
|
17
|
+
resourceUri: string;
|
|
18
|
+
};
|
|
19
|
+
_meta?: {
|
|
20
|
+
ui?: {
|
|
21
|
+
resourceUri?: string;
|
|
22
|
+
};
|
|
23
|
+
'ui/resourceUri'?: string;
|
|
24
|
+
};
|
|
25
|
+
}>;
|
|
26
|
+
}>;
|
|
27
|
+
sseClient?: SSEClient | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface McpAppMetadata {
|
|
31
|
+
toolName: string;
|
|
32
|
+
resourceUri: string;
|
|
33
|
+
sessionId: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface McpAppRendererProps {
|
|
37
|
+
metadata: McpAppMetadata;
|
|
38
|
+
input?: Record<string, unknown>;
|
|
39
|
+
result?: unknown;
|
|
40
|
+
status: 'executing' | 'inProgress' | 'complete' | 'idle';
|
|
41
|
+
sseClient?: SSEClient | null;
|
|
42
|
+
/** Custom CSS class for the container */
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Internal component that renders the MCP app in a sandboxed iframe
|
|
48
|
+
*/
|
|
49
|
+
const McpAppRenderer = memo(function McpAppRenderer({
|
|
50
|
+
metadata,
|
|
51
|
+
input,
|
|
52
|
+
result,
|
|
53
|
+
status,
|
|
54
|
+
sseClient,
|
|
55
|
+
className,
|
|
56
|
+
}: McpAppRendererProps) {
|
|
57
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
58
|
+
const { host, error: hostError } = useAppHost(sseClient as SSEClient, iframeRef);
|
|
59
|
+
const [isLaunched, setIsLaunched] = useState(false);
|
|
60
|
+
const [error, setError] = useState<Error | null>(null);
|
|
61
|
+
|
|
62
|
+
// Track which data has been sent to prevent duplicates
|
|
63
|
+
const sentInputRef = useRef(false);
|
|
64
|
+
const sentResultRef = useRef(false);
|
|
65
|
+
const lastInputRef = useRef(input);
|
|
66
|
+
const lastResultRef = useRef(result);
|
|
67
|
+
const lastStatusRef = useRef(status);
|
|
68
|
+
|
|
69
|
+
// Launch the app when host is ready
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!host || !metadata.resourceUri || !metadata.sessionId) return;
|
|
72
|
+
|
|
73
|
+
host
|
|
74
|
+
.launch(metadata.resourceUri, metadata.sessionId)
|
|
75
|
+
.then(() => setIsLaunched(true))
|
|
76
|
+
.catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
|
|
77
|
+
}, [host, metadata.resourceUri, metadata.sessionId]);
|
|
78
|
+
|
|
79
|
+
// Send tool input when available or when it changes
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!host || !isLaunched || !input) return;
|
|
82
|
+
|
|
83
|
+
// Send if never sent, or if input changed
|
|
84
|
+
if (!sentInputRef.current || JSON.stringify(input) !== JSON.stringify(lastInputRef.current)) {
|
|
85
|
+
sentInputRef.current = true;
|
|
86
|
+
lastInputRef.current = input;
|
|
87
|
+
host.sendToolInput(input);
|
|
88
|
+
}
|
|
89
|
+
}, [host, isLaunched, input]);
|
|
90
|
+
|
|
91
|
+
// Send tool result when complete or when it changes
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!host || !isLaunched || result === undefined) return;
|
|
94
|
+
if (status !== 'complete') return;
|
|
95
|
+
|
|
96
|
+
// Send if never sent, or if result changed
|
|
97
|
+
if (!sentResultRef.current || JSON.stringify(result) !== JSON.stringify(lastResultRef.current)) {
|
|
98
|
+
sentResultRef.current = true;
|
|
99
|
+
lastResultRef.current = result;
|
|
100
|
+
const formattedResult =
|
|
101
|
+
typeof result === 'string'
|
|
102
|
+
? { content: [{ type: 'text', text: result }] }
|
|
103
|
+
: result;
|
|
104
|
+
host.sendToolResult(formattedResult);
|
|
105
|
+
}
|
|
106
|
+
}, [host, isLaunched, result, status]);
|
|
107
|
+
|
|
108
|
+
// Reset sent flags when tool status resets to executing (new tool call)
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (status === 'executing' && lastStatusRef.current !== 'executing') {
|
|
111
|
+
sentInputRef.current = false;
|
|
112
|
+
sentResultRef.current = false;
|
|
113
|
+
}
|
|
114
|
+
lastStatusRef.current = status;
|
|
115
|
+
}, [status]);
|
|
116
|
+
|
|
117
|
+
// Display errors
|
|
118
|
+
const displayError = error || hostError;
|
|
119
|
+
if (displayError) {
|
|
120
|
+
return (
|
|
121
|
+
<div className={`p-4 bg-red-900/20 border border-red-700 rounded text-red-200 ${className || ''}`}>
|
|
122
|
+
Error: {displayError.message || String(displayError)}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className={`w-full border border-gray-700 rounded overflow-hidden bg-white min-h-96 my-2 relative ${className || ''}`}>
|
|
129
|
+
<iframe
|
|
130
|
+
ref={iframeRef}
|
|
131
|
+
sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads"
|
|
132
|
+
className="w-full h-full min-h-96"
|
|
133
|
+
style={{ height: 'auto' }}
|
|
134
|
+
title="MCP App"
|
|
135
|
+
/>
|
|
136
|
+
{!isLaunched && (
|
|
137
|
+
<div className="absolute inset-0 bg-gray-900/50 flex items-center justify-center pointer-events-none">
|
|
138
|
+
<div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Simple hook to get MCP app metadata
|
|
147
|
+
*
|
|
148
|
+
* @param mcpClient - The MCP client from useMcp() or context
|
|
149
|
+
* @returns Object with getAppMetadata function and McpAppRenderer component
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```tsx
|
|
153
|
+
* function ToolRenderer(props) {
|
|
154
|
+
* const { getAppMetadata, McpAppRenderer } = useMcpApps(mcpClient);
|
|
155
|
+
* const metadata = getAppMetadata(props.name);
|
|
156
|
+
*
|
|
157
|
+
* if (!metadata) return null;
|
|
158
|
+
* return (
|
|
159
|
+
* <McpAppRenderer
|
|
160
|
+
* metadata={metadata}
|
|
161
|
+
* input={props.args}
|
|
162
|
+
* result={props.result}
|
|
163
|
+
* status={props.status}
|
|
164
|
+
* />
|
|
165
|
+
* );
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export function useMcpApps(mcpClient: McpClient | null) {
|
|
170
|
+
/**
|
|
171
|
+
* Get MCP app metadata for a tool name
|
|
172
|
+
* This is fast and can be called on every render
|
|
173
|
+
*/
|
|
174
|
+
const getAppMetadata = useCallback(
|
|
175
|
+
(toolName: string): McpAppMetadata | undefined => {
|
|
176
|
+
if (!mcpClient) return undefined;
|
|
177
|
+
|
|
178
|
+
const extractedName = extractToolName(toolName);
|
|
179
|
+
|
|
180
|
+
for (const conn of mcpClient.connections) {
|
|
181
|
+
for (const tool of conn.tools) {
|
|
182
|
+
const candidateName = extractToolName(tool.name);
|
|
183
|
+
// Check both locations: direct mcpApp or _meta.ui
|
|
184
|
+
const resourceUri =
|
|
185
|
+
tool.mcpApp?.resourceUri ??
|
|
186
|
+
tool._meta?.ui?.resourceUri ??
|
|
187
|
+
tool._meta?.['ui/resourceUri'];
|
|
188
|
+
|
|
189
|
+
if (resourceUri && candidateName === extractedName) {
|
|
190
|
+
return {
|
|
191
|
+
toolName: candidateName,
|
|
192
|
+
resourceUri,
|
|
193
|
+
sessionId: conn.sessionId,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return undefined;
|
|
200
|
+
},
|
|
201
|
+
[mcpClient]
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return { getAppMetadata, McpAppRenderer };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Extract the base tool name, removing any prefixes
|
|
209
|
+
*/
|
|
210
|
+
function extractToolName(fullName: string): string {
|
|
211
|
+
// Handle patterns like "tool_abc123_get-time" -> "get-time"
|
|
212
|
+
const match = fullName.match(/(?:tool_[^_]+_)?(.+)$/);
|
|
213
|
+
return match?.[1] || fullName;
|
|
214
|
+
}
|
|
@@ -64,6 +64,13 @@ export interface UseMcpOptions {
|
|
|
64
64
|
* @default 60000
|
|
65
65
|
*/
|
|
66
66
|
requestTimeout?: number;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Enable client debug logs.
|
|
70
|
+
* @default false
|
|
71
|
+
*/
|
|
72
|
+
debug?: boolean;
|
|
73
|
+
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
export interface McpConnection {
|
|
@@ -74,8 +81,9 @@ export interface McpConnection {
|
|
|
74
81
|
transport?: string;
|
|
75
82
|
state: McpConnectionState;
|
|
76
83
|
tools: ToolInfo[];
|
|
84
|
+
authUrl?: string;
|
|
77
85
|
error?: string;
|
|
78
|
-
|
|
86
|
+
createdAt?: Date;
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
export interface McpClient {
|
|
@@ -150,6 +158,11 @@ export interface McpClient {
|
|
|
150
158
|
*/
|
|
151
159
|
finishAuth: (sessionId: string, code: string) => Promise<FinishAuthResult>;
|
|
152
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Explicitly resume OAuth flow for an existing session
|
|
163
|
+
*/
|
|
164
|
+
resumeAuth: (sessionId: string) => Promise<void>;
|
|
165
|
+
|
|
153
166
|
/**
|
|
154
167
|
* Call a tool from a session
|
|
155
168
|
*/
|
|
@@ -207,6 +220,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
207
220
|
|
|
208
221
|
const clientRef = useRef<SSEClient | null>(null);
|
|
209
222
|
const isMountedRef = useRef(true);
|
|
223
|
+
const suppressAuthRedirectSessionsRef = useRef<Set<string>>(new Set());
|
|
210
224
|
|
|
211
225
|
const [connections, setConnections] = useState<McpConnection[]>([]);
|
|
212
226
|
const [status, setStatus] = useState<'connecting' | 'connected' | 'disconnected' | 'error'>(
|
|
@@ -239,7 +253,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
239
253
|
setStatus(newStatus);
|
|
240
254
|
}
|
|
241
255
|
},
|
|
242
|
-
|
|
256
|
+
debug: options.debug,
|
|
243
257
|
};
|
|
244
258
|
|
|
245
259
|
const client = new SSEClient(clientOptions);
|
|
@@ -262,25 +276,52 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
262
276
|
/**
|
|
263
277
|
* Update connections based on event
|
|
264
278
|
*/
|
|
265
|
-
const updateConnectionsFromEvent = useCallback((event: McpConnectionEvent) => {
|
|
266
|
-
if (!isMountedRef.current) return;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
279
|
+
const updateConnectionsFromEvent = useCallback((event: McpConnectionEvent) => {
|
|
280
|
+
if (!isMountedRef.current) return;
|
|
281
|
+
|
|
282
|
+
const isTransientReconnectState = (state: McpConnectionState): boolean =>
|
|
283
|
+
state === 'INITIALIZING' ||
|
|
284
|
+
state === 'VALIDATING' ||
|
|
285
|
+
state === 'RECONNECTING' ||
|
|
286
|
+
state === 'CONNECTING' ||
|
|
287
|
+
state === 'CONNECTED' ||
|
|
288
|
+
state === 'DISCOVERING';
|
|
289
|
+
|
|
290
|
+
setConnections((prev: McpConnection[]) => {
|
|
291
|
+
switch (event.type) {
|
|
292
|
+
case 'state_changed': {
|
|
293
|
+
const existing = prev.find((c: McpConnection) => c.sessionId === event.sessionId);
|
|
294
|
+
if (existing) {
|
|
295
|
+
// In stateless per-request transport, tool calls can emit transient reconnect states.
|
|
296
|
+
// Keep READY sticky to avoid UI flicker from READY -> CONNECTING -> CONNECTED.
|
|
297
|
+
const nextState =
|
|
298
|
+
existing.state === 'READY' && isTransientReconnectState(event.state)
|
|
299
|
+
? existing.state
|
|
300
|
+
: event.state;
|
|
301
|
+
|
|
302
|
+
return prev.map((c: McpConnection) =>
|
|
303
|
+
c.sessionId === event.sessionId ? {
|
|
304
|
+
...c,
|
|
305
|
+
state: nextState,
|
|
306
|
+
// update createdAt if present in event, otherwise keep existing
|
|
307
|
+
createdAt: event.createdAt ? new Date(event.createdAt) : c.createdAt
|
|
308
|
+
} : c
|
|
275
309
|
);
|
|
276
310
|
} else {
|
|
311
|
+
// Fix: Don't add back disconnected sessions that were just removed
|
|
312
|
+
if (event.state === 'DISCONNECTED') {
|
|
313
|
+
return prev;
|
|
314
|
+
}
|
|
315
|
+
|
|
277
316
|
return [
|
|
278
317
|
...prev,
|
|
279
318
|
{
|
|
280
319
|
sessionId: event.sessionId,
|
|
281
320
|
serverId: event.serverId,
|
|
282
321
|
serverName: event.serverName,
|
|
322
|
+
serverUrl: event.serverUrl,
|
|
283
323
|
state: event.state,
|
|
324
|
+
createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
|
|
284
325
|
tools: [],
|
|
285
326
|
},
|
|
286
327
|
];
|
|
@@ -303,14 +344,17 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
303
344
|
if (event.authUrl) {
|
|
304
345
|
onLog?.('info', `OAuth required - redirecting to ${event.authUrl}`, { authUrl: event.authUrl });
|
|
305
346
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
347
|
+
// Suppress redirects/popups for auto-restore on page load.
|
|
348
|
+
if (!suppressAuthRedirectSessionsRef.current.has(event.sessionId)) {
|
|
349
|
+
if (onRedirect) {
|
|
350
|
+
onRedirect(event.authUrl);
|
|
351
|
+
} else if (typeof window !== 'undefined') {
|
|
352
|
+
window.location.href = event.authUrl;
|
|
353
|
+
}
|
|
310
354
|
}
|
|
311
355
|
}
|
|
312
356
|
return prev.map((c: McpConnection) =>
|
|
313
|
-
c.sessionId === event.sessionId ? { ...c, state: 'AUTHENTICATING' } : c
|
|
357
|
+
c.sessionId === event.sessionId ? { ...c, state: 'AUTHENTICATING', authUrl: event.authUrl } : c
|
|
314
358
|
);
|
|
315
359
|
}
|
|
316
360
|
|
|
@@ -328,7 +372,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
328
372
|
return prev;
|
|
329
373
|
}
|
|
330
374
|
});
|
|
331
|
-
}, [onLog]);
|
|
375
|
+
}, [onLog, onRedirect]);
|
|
332
376
|
|
|
333
377
|
/**
|
|
334
378
|
* Load sessions from server
|
|
@@ -351,7 +395,8 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
351
395
|
serverName: s.serverName ?? 'Unknown Server',
|
|
352
396
|
serverUrl: s.serverUrl,
|
|
353
397
|
transport: s.transport,
|
|
354
|
-
state: 'VALIDATING' as McpConnectionState,
|
|
398
|
+
state: (s.active === false ? 'AUTHENTICATING' : 'VALIDATING') as McpConnectionState,
|
|
399
|
+
createdAt: new Date(s.createdAt),
|
|
355
400
|
tools: [],
|
|
356
401
|
}))
|
|
357
402
|
);
|
|
@@ -362,9 +407,16 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
362
407
|
sessions.map(async (session: SessionInfo) => {
|
|
363
408
|
if (clientRef.current) {
|
|
364
409
|
try {
|
|
410
|
+
// Pending auth sessions should not auto-trigger popup/redirect on reload.
|
|
411
|
+
if (session.active === false) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
suppressAuthRedirectSessionsRef.current.add(session.sessionId);
|
|
365
415
|
await clientRef.current.restoreSession(session.sessionId);
|
|
366
416
|
} catch (error) {
|
|
367
417
|
console.error(`[useMcp] Failed to validate session ${session.sessionId}:`, error);
|
|
418
|
+
} finally {
|
|
419
|
+
suppressAuthRedirectSessionsRef.current.delete(session.sessionId);
|
|
368
420
|
}
|
|
369
421
|
}
|
|
370
422
|
})
|
|
@@ -448,6 +500,18 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
448
500
|
return await clientRef.current.finishAuth(sessionId, code);
|
|
449
501
|
}, []);
|
|
450
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Explicit user action to resume OAuth for an existing pending session.
|
|
505
|
+
*/
|
|
506
|
+
const resumeAuth = useCallback(async (sessionId: string): Promise<void> => {
|
|
507
|
+
if (!clientRef.current) {
|
|
508
|
+
throw new Error('SSE client not initialized');
|
|
509
|
+
}
|
|
510
|
+
// Ensure this attempt is not suppressed as background restore.
|
|
511
|
+
suppressAuthRedirectSessionsRef.current.delete(sessionId);
|
|
512
|
+
await clientRef.current.restoreSession(sessionId);
|
|
513
|
+
}, []);
|
|
514
|
+
|
|
451
515
|
/**
|
|
452
516
|
* Call a tool
|
|
453
517
|
*/
|
|
@@ -565,6 +629,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
565
629
|
connectSSE,
|
|
566
630
|
disconnectSSE,
|
|
567
631
|
finishAuth,
|
|
632
|
+
resumeAuth,
|
|
568
633
|
callTool,
|
|
569
634
|
listTools,
|
|
570
635
|
listPrompts,
|