@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 CHANGED
@@ -1,4 +1,4 @@
1
- # @flair/mcp-client
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 @flair/mcp-client
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 "@flair/mcp-client/server";
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 '@flair/mcp-client';
59
- import '@flair/mcp-client/dist/styles/MCPChat.css';
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 '@flair/mcp-client/dist/styles/MCPChat.css';
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 '@flair/mcp-client/server';
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 '@flair/mcp-client/server';
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();
@@ -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,qBAgTd"}
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
- addThinkingStep("🤔 Analyzing your question...");
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: isLoading || !input.trim() }, "Send"))));
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,uBAiE/B;AAED;;GAEG;AACH,wBAAgB,qBAAqB,KACrB,SAAS,OAAO,uBAU/B;AAED;;GAEG;AACH,wBAAsB,iBAAiB,kBAKtC"}
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"}
@@ -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
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type, ...data })}\n\n`));
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
- sendEvent("error", {
49
- message: error instanceof Error ? error.message : "An error occurred",
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: {
@@ -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;IAqL1F,YAAY,IAAI,IAAI;IAId,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAG/B"}
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"}
@@ -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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nqminds/mcp-client",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Reusable MCP client component with AI chat interface",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",