@nqminds/mcp-client 1.0.4 → 1.0.5
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 +8 -8
- package/dist/MCPChat.d.ts.map +1 -1
- package/dist/MCPChat.js +25 -3
- package/dist/api-helpers.d.ts.map +1 -1
- package/dist/api-helpers.js +35 -6
- package/dist/openai-client.d.ts +1 -1
- package/dist/openai-client.d.ts.map +1 -1
- package/dist/openai-client.js +13 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @nqminds/mcp-client
|
|
2
2
|
|
|
3
3
|
A complete, ready-to-use React component and backend client for MCP (Model Context Protocol) AI chat with OpenAI integration. Includes both the UI component and the OpenAI-powered backend logic.
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ A complete, ready-to-use React component and backend client for MCP (Model Conte
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install @
|
|
16
|
+
npm install @nqminds/mcp-client
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Quick Start
|
|
@@ -33,7 +33,7 @@ OPENAI_MODEL=chatgpt-5-mini
|
|
|
33
33
|
Create `app/api/mcp/chat/route.ts`:
|
|
34
34
|
|
|
35
35
|
```typescript
|
|
36
|
-
import { createMCPChatHandler, createMCPClearHandler } from "@
|
|
36
|
+
import { createMCPChatHandler, createMCPClearHandler } from "@nqminds/mcp-client/server";
|
|
37
37
|
|
|
38
38
|
const chatHandler = createMCPChatHandler({
|
|
39
39
|
openaiApiKey: process.env.OPENAI_API_KEY!,
|
|
@@ -55,8 +55,8 @@ export async function DELETE(req: Request) {
|
|
|
55
55
|
### 3. Add the component to your app
|
|
56
56
|
|
|
57
57
|
```tsx
|
|
58
|
-
import { MCPChat } from '@
|
|
59
|
-
import '@
|
|
58
|
+
import { MCPChat } from '@nqminds/mcp-client';
|
|
59
|
+
import '@nqminds/mcp-client/dist/styles/MCPChat.css';
|
|
60
60
|
|
|
61
61
|
export default function Page() {
|
|
62
62
|
return (
|
|
@@ -71,7 +71,7 @@ export default function Page() {
|
|
|
71
71
|
### 4. Import CSS in your layout
|
|
72
72
|
|
|
73
73
|
```tsx
|
|
74
|
-
import '@
|
|
74
|
+
import '@nqminds/mcp-client/dist/styles/MCPChat.css';
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
That's it! Your MCP chat is ready to use.
|
|
@@ -117,7 +117,7 @@ Available CSS variables:
|
|
|
117
117
|
If you need more control, use the client directly (server-side only):
|
|
118
118
|
|
|
119
119
|
```typescript
|
|
120
|
-
import { MCPClientOpenAI } from '@
|
|
120
|
+
import { MCPClientOpenAI } from '@nqminds/mcp-client/server';
|
|
121
121
|
|
|
122
122
|
const client = new MCPClientOpenAI({
|
|
123
123
|
openaiApiKey: process.env.OPENAI_API_KEY!,
|
|
@@ -138,7 +138,7 @@ await client.cleanup();
|
|
|
138
138
|
Create your own streaming handler (server-side):
|
|
139
139
|
|
|
140
140
|
```typescript
|
|
141
|
-
import { MCPClientOpenAI } from '@
|
|
141
|
+
import { MCPClientOpenAI } from '@nqminds/mcp-client/server';
|
|
142
142
|
|
|
143
143
|
export async function POST(req: Request) {
|
|
144
144
|
const { message } = await req.json();
|
package/dist/MCPChat.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MCPChat.d.ts","sourceRoot":"","sources":["../src/MCPChat.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAE3D,OAAO,KAAK,EAAyB,YAAY,EAAe,MAAM,SAAS,CAAC;AAEhF,wBAAgB,OAAO,CAAC,EACtB,aAAa,EACb,WAA6B,EAC7B,YAAiB,EACjB,SAAc,EACf,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"MCPChat.d.ts","sourceRoot":"","sources":["../src/MCPChat.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAE3D,OAAO,KAAK,EAAyB,YAAY,EAAe,MAAM,SAAS,CAAC;AAEhF,wBAAgB,OAAO,CAAC,EACtB,aAAa,EACb,WAA6B,EAC7B,YAAiB,EACjB,SAAc,EACf,EAAE,YAAY,qBAoVd"}
|
package/dist/MCPChat.js
CHANGED
|
@@ -9,6 +9,7 @@ export function MCPChat({ companyNumber, apiEndpoint = "/api/mcp/chat", customSt
|
|
|
9
9
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
10
10
|
const messagesEndRef = useRef(null);
|
|
11
11
|
const thinkingEndRef = useRef(null);
|
|
12
|
+
const abortControllerRef = useRef(null);
|
|
12
13
|
// Merge custom styles with default CSS variables
|
|
13
14
|
const containerStyle = {
|
|
14
15
|
...customStyles,
|
|
@@ -19,10 +20,22 @@ export function MCPChat({ companyNumber, apiEndpoint = "/api/mcp/chat", customSt
|
|
|
19
20
|
useEffect(() => {
|
|
20
21
|
scrollToBottom();
|
|
21
22
|
}, [messages]);
|
|
23
|
+
const cancelRequest = () => {
|
|
24
|
+
if (abortControllerRef.current) {
|
|
25
|
+
abortControllerRef.current.abort();
|
|
26
|
+
abortControllerRef.current = null;
|
|
27
|
+
setIsLoading(false);
|
|
28
|
+
setThinkingSteps([]);
|
|
29
|
+
// Remove any streaming message that was in progress
|
|
30
|
+
setMessages((prev) => prev.filter((m) => !m.isStreaming));
|
|
31
|
+
}
|
|
32
|
+
};
|
|
22
33
|
const handleSubmit = async (e) => {
|
|
23
34
|
e.preventDefault();
|
|
24
35
|
if (!input.trim() || isLoading)
|
|
25
36
|
return;
|
|
37
|
+
// Set loading state immediately to show cancel button
|
|
38
|
+
setIsLoading(true);
|
|
26
39
|
const userMessage = {
|
|
27
40
|
role: "user",
|
|
28
41
|
content: input.trim(),
|
|
@@ -30,7 +43,6 @@ export function MCPChat({ companyNumber, apiEndpoint = "/api/mcp/chat", customSt
|
|
|
30
43
|
};
|
|
31
44
|
setMessages((prev) => [...prev, userMessage]);
|
|
32
45
|
setInput("");
|
|
33
|
-
setIsLoading(true);
|
|
34
46
|
setThinkingSteps([]);
|
|
35
47
|
// Add initial thinking step
|
|
36
48
|
let thinkingStepCounter = 0;
|
|
@@ -44,7 +56,9 @@ export function MCPChat({ companyNumber, apiEndpoint = "/api/mcp/chat", customSt
|
|
|
44
56
|
thinkingEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
45
57
|
}, 50);
|
|
46
58
|
};
|
|
47
|
-
|
|
59
|
+
// Create abort controller for this request
|
|
60
|
+
const abortController = new AbortController();
|
|
61
|
+
abortControllerRef.current = abortController;
|
|
48
62
|
try {
|
|
49
63
|
// Call your API route that communicates with MCP
|
|
50
64
|
const response = await fetch(apiEndpoint, {
|
|
@@ -54,6 +68,7 @@ export function MCPChat({ companyNumber, apiEndpoint = "/api/mcp/chat", customSt
|
|
|
54
68
|
message: userMessage.content,
|
|
55
69
|
context: companyNumber ? { company_number: companyNumber } : undefined,
|
|
56
70
|
}),
|
|
71
|
+
signal: abortController.signal,
|
|
57
72
|
});
|
|
58
73
|
if (!response.ok) {
|
|
59
74
|
throw new Error("Failed to get response");
|
|
@@ -135,6 +150,11 @@ export function MCPChat({ companyNumber, apiEndpoint = "/api/mcp/chat", customSt
|
|
|
135
150
|
}
|
|
136
151
|
}
|
|
137
152
|
catch (error) {
|
|
153
|
+
// Don't show error message if request was cancelled
|
|
154
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
155
|
+
console.log("Request was cancelled");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
138
158
|
console.error("Error:", error);
|
|
139
159
|
const errorMessage = {
|
|
140
160
|
role: "assistant",
|
|
@@ -148,6 +168,8 @@ export function MCPChat({ companyNumber, apiEndpoint = "/api/mcp/chat", customSt
|
|
|
148
168
|
});
|
|
149
169
|
}
|
|
150
170
|
finally {
|
|
171
|
+
// Clean up abort controller
|
|
172
|
+
abortControllerRef.current = null;
|
|
151
173
|
setIsLoading(false);
|
|
152
174
|
// Clear thinking steps after a brief delay
|
|
153
175
|
setTimeout(() => setThinkingSteps([]), 2000);
|
|
@@ -203,5 +225,5 @@ export function MCPChat({ companyNumber, apiEndpoint = "/api/mcp/chat", customSt
|
|
|
203
225
|
React.createElement("div", { ref: messagesEndRef })),
|
|
204
226
|
React.createElement("form", { onSubmit: handleSubmit, className: "mcp-chat-input-form" },
|
|
205
227
|
React.createElement("input", { type: "text", value: input, onChange: (e) => setInput(e.target.value), placeholder: "Ask a question...", className: "mcp-chat-input", disabled: isLoading }),
|
|
206
|
-
React.createElement("button", { type: "submit", className: "mcp-chat-button mcp-chat-button-primary", disabled:
|
|
228
|
+
isLoading ? (React.createElement("button", { type: "button", onClick: cancelRequest, className: "mcp-chat-button mcp-chat-button-secondary" }, "Cancel")) : (React.createElement("button", { type: "submit", className: "mcp-chat-button mcp-chat-button-primary", disabled: !input.trim() }, "Send")))));
|
|
207
229
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-helpers.d.ts","sourceRoot":"","sources":["../src/api-helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,sBAAsB,IACnD,SAAS,OAAO,
|
|
1
|
+
{"version":3,"file":"api-helpers.d.ts","sourceRoot":"","sources":["../src/api-helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,sBAAsB,IACnD,SAAS,OAAO,uBAgG/B;AAED;;GAEG;AACH,wBAAgB,qBAAqB,KACrB,SAAS,OAAO,uBAU/B;AAED;;GAEG;AACH,wBAAsB,iBAAiB,kBAKtC"}
|
package/dist/api-helpers.js
CHANGED
|
@@ -21,22 +21,43 @@ export function createMCPChatHandler(config) {
|
|
|
21
21
|
await client.connect();
|
|
22
22
|
clients.set(sessionId, client);
|
|
23
23
|
}
|
|
24
|
+
// Create an AbortController to handle client disconnection
|
|
25
|
+
const abortController = new AbortController();
|
|
24
26
|
// Create a streaming response
|
|
25
27
|
const stream = new ReadableStream({
|
|
26
28
|
async start(controller) {
|
|
27
29
|
const encoder = new TextEncoder();
|
|
28
30
|
const sendEvent = (type, data) => {
|
|
29
|
-
|
|
31
|
+
// Check if client disconnected before sending more data
|
|
32
|
+
if (abortController.signal.aborted) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type, ...data })}\n\n`));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
// Client disconnected, abort processing
|
|
40
|
+
abortController.abort();
|
|
41
|
+
}
|
|
30
42
|
};
|
|
31
43
|
try {
|
|
32
44
|
sendEvent("thinking", { message: "🤔 Analyzing your question..." });
|
|
33
|
-
// Process the query with thinking callback
|
|
45
|
+
// Process the query with thinking callback and abort signal
|
|
34
46
|
const response = await client.processQuery(context ? `${message}\nContext: ${JSON.stringify(context)}` : message, (thinkingMessage) => {
|
|
35
47
|
sendEvent("thinking", { message: thinkingMessage });
|
|
36
|
-
}
|
|
48
|
+
}, abortController.signal // Pass abort signal to enable cancellation
|
|
49
|
+
);
|
|
50
|
+
// Check if aborted before streaming response
|
|
51
|
+
if (abortController.signal.aborted) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
37
54
|
// Stream the response in chunks
|
|
38
55
|
const chunkSize = 10;
|
|
39
56
|
for (let i = 0; i < response.length; i += chunkSize) {
|
|
57
|
+
// Check for cancellation between chunks
|
|
58
|
+
if (abortController.signal.aborted) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
40
61
|
const chunk = response.slice(i, i + chunkSize);
|
|
41
62
|
sendEvent("content", { chunk });
|
|
42
63
|
// Small delay for better streaming effect
|
|
@@ -45,14 +66,22 @@ export function createMCPChatHandler(config) {
|
|
|
45
66
|
sendEvent("done", {});
|
|
46
67
|
}
|
|
47
68
|
catch (error) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
// Don't send error if request was aborted
|
|
70
|
+
if (!abortController.signal.aborted) {
|
|
71
|
+
sendEvent("error", {
|
|
72
|
+
message: error instanceof Error ? error.message : "An error occurred",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
51
75
|
}
|
|
52
76
|
finally {
|
|
53
77
|
controller.close();
|
|
54
78
|
}
|
|
55
79
|
},
|
|
80
|
+
cancel() {
|
|
81
|
+
// This is called when client disconnects/cancels
|
|
82
|
+
console.log("Client disconnected, aborting server processing");
|
|
83
|
+
abortController.abort();
|
|
84
|
+
}
|
|
56
85
|
});
|
|
57
86
|
return new Response(stream, {
|
|
58
87
|
headers: {
|
package/dist/openai-client.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare class MCPClientOpenAI {
|
|
|
19
19
|
constructor(config: MCPClientConfig);
|
|
20
20
|
private compactConversation;
|
|
21
21
|
connect(): Promise<void>;
|
|
22
|
-
processQuery(query: string, onThinking?: (message: string) => void): Promise<string>;
|
|
22
|
+
processQuery(query: string, onThinking?: (message: string) => void, abortSignal?: AbortSignal): Promise<string>;
|
|
23
23
|
clearHistory(): void;
|
|
24
24
|
cleanup(): Promise<void>;
|
|
25
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai-client.d.ts","sourceRoot":"","sources":["../src/openai-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,mBAAmB,CAAsB;IACjD,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,MAAM,CAA4B;gBAE9B,MAAM,EAAE,eAAe;YAkCrB,mBAAmB;IAoB3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"openai-client.d.ts","sourceRoot":"","sources":["../src/openai-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,mBAAmB,CAAsB;IACjD,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,MAAM,CAA4B;gBAE9B,MAAM,EAAE,eAAe;YAkCrB,mBAAmB;IAoB3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EAAE,WAAW,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IAoMrH,YAAY,IAAI,IAAI;IAId,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAG/B"}
|
package/dist/openai-client.js
CHANGED
|
@@ -56,7 +56,11 @@ export class MCPClientOpenAI {
|
|
|
56
56
|
async connect() {
|
|
57
57
|
await this.client.connect(this.transport);
|
|
58
58
|
}
|
|
59
|
-
async processQuery(query, onThinking) {
|
|
59
|
+
async processQuery(query, onThinking, abortSignal) {
|
|
60
|
+
// Check for cancellation at start
|
|
61
|
+
if (abortSignal?.aborted) {
|
|
62
|
+
throw new Error("Request was cancelled");
|
|
63
|
+
}
|
|
60
64
|
// Check if we should compact
|
|
61
65
|
const shouldCompact = this.conversationHistory.length >= 40 &&
|
|
62
66
|
(Date.now() - this.lastCompaction > 10 * 60 * 1000);
|
|
@@ -91,6 +95,10 @@ export class MCPClientOpenAI {
|
|
|
91
95
|
let outOfToolCalls = false;
|
|
92
96
|
while (loopCount < maxLoops) {
|
|
93
97
|
loopCount++;
|
|
98
|
+
// Check for cancellation before each API call
|
|
99
|
+
if (abortSignal?.aborted) {
|
|
100
|
+
throw new Error("Request was cancelled");
|
|
101
|
+
}
|
|
94
102
|
// Call OpenAI Responses API with error handling
|
|
95
103
|
let response;
|
|
96
104
|
try {
|
|
@@ -140,6 +148,10 @@ export class MCPClientOpenAI {
|
|
|
140
148
|
if (functionCalls.length > 0) {
|
|
141
149
|
this.conversationHistory.push(...output);
|
|
142
150
|
for (const functionCall of functionCalls) {
|
|
151
|
+
// Check for cancellation before each tool call
|
|
152
|
+
if (abortSignal?.aborted) {
|
|
153
|
+
throw new Error("Request was cancelled");
|
|
154
|
+
}
|
|
143
155
|
const functionName = functionCall.name;
|
|
144
156
|
const functionArgs = typeof functionCall.arguments === 'string'
|
|
145
157
|
? JSON.parse(functionCall.arguments)
|