@nqminds/mcp-client 1.0.4 → 1.0.7

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,qBAmVd"}
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);
@@ -184,11 +206,9 @@ export function MCPChat({ companyNumber, apiEndpoint = "/api/mcp/chat", customSt
184
206
  "Currently viewing: ",
185
207
  React.createElement("strong", null, companyNumber))),
186
208
  React.createElement("p", { className: "mcp-chat-welcome-subtitle" }, "Try asking:"),
187
- React.createElement("ul", { className: "mcp-chat-suggestions" },
188
- React.createElement("li", null, "\u201CSay hello\u201D"),
189
- companyNumber && (React.createElement(React.Fragment, null,
190
- React.createElement("li", null, "\u201CGet company info\u201D"),
191
- React.createElement("li", null, "\u201CShow me financial data from 2020\u201D")))))),
209
+ React.createElement("ul", { className: "mcp-chat-suggestions" }, companyNumber && (React.createElement(React.Fragment, null,
210
+ React.createElement("li", null, "\u201CGet company info\u201D"),
211
+ React.createElement("li", null, "\u201CShow me financial data from 2020\u201D")))))),
192
212
  messages.map((msg, idx) => (React.createElement("div", { key: idx, className: `mcp-chat-message ${msg.role === "user" ? "mcp-chat-message-user" : "mcp-chat-message-assistant"}` },
193
213
  React.createElement("div", { className: "mcp-chat-message-bubble" },
194
214
  msg.role === "assistant" ? (React.createElement("div", { className: "mcp-chat-message-content markdown-content" },
@@ -203,5 +223,5 @@ export function MCPChat({ companyNumber, apiEndpoint = "/api/mcp/chat", customSt
203
223
  React.createElement("div", { ref: messagesEndRef })),
204
224
  React.createElement("form", { onSubmit: handleSubmit, className: "mcp-chat-input-form" },
205
225
  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"))));
226
+ 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
227
  }
@@ -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;YAwErB,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;IAMd,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAG/B"}
@@ -33,6 +33,43 @@ export class MCPClientOpenAI {
33
33
  }, {
34
34
  capabilities: {},
35
35
  });
36
+ // Initialize conversation with system message
37
+ this.conversationHistory = [
38
+ {
39
+ type: "message",
40
+ role: "system",
41
+ content: [
42
+ {
43
+ type: "input_text",
44
+ text: `You are a helpful assistant with access to Companies House data through specialized tools.
45
+
46
+ CRITICAL CONTEXT AWARENESS RULES:
47
+ 1. Carefully track ALL entities you mention in your responses (company numbers, names, people, dates, etc.)
48
+ 2. When the user refers to "that company," "the person," "those results," or uses similar references, ALWAYS look back at what you just discussed in the immediately preceding messages
49
+ 3. If you mentioned specific company numbers, names, or other identifiers, remember them for follow-up questions
50
+ 4. Before saying "I don't have a record of X," review your recent responses to check if you did mention it
51
+ 5. Maintain awareness of the conversation flow - if you just provided information about something, the user's next question likely refers to it
52
+
53
+ RESPONSE FORMATTING RULES:
54
+ - NEVER show raw JSON data to users unless they explicitly ask for "JSON", "raw data", or similar
55
+ - Always present data in natural, human-readable format using clear sentences and bullet points
56
+ - Convert dates to readable format (e.g., "15 March 2023" instead of "2023-03-15")
57
+ - Format addresses as natural text, not as structured fields
58
+ - Present lists of items as bulleted lists or comma-separated values
59
+ - Use tables/structured format only when it significantly improves readability
60
+ - When showing company officers or PSCs, write like: "John Smith (born March 1975, British nationality)" rather than showing field names
61
+ - Only include the most relevant information - don't dump all available fields
62
+
63
+ When responding:
64
+ - Be concise and direct
65
+ - Use tools to fetch accurate, up-to-date Companies House data
66
+ - Track key identifiers (company numbers, PSC names, etc.) across the conversation
67
+ - If unclear what the user is referring to, check your previous response first before asking for clarification
68
+ - Never expose internal implementation details like "MCP Server" or tool names to users`,
69
+ },
70
+ ],
71
+ },
72
+ ];
36
73
  }
37
74
  async compactConversation() {
38
75
  try {
@@ -56,7 +93,11 @@ export class MCPClientOpenAI {
56
93
  async connect() {
57
94
  await this.client.connect(this.transport);
58
95
  }
59
- async processQuery(query, onThinking) {
96
+ async processQuery(query, onThinking, abortSignal) {
97
+ // Check for cancellation at start
98
+ if (abortSignal?.aborted) {
99
+ throw new Error("Request was cancelled");
100
+ }
60
101
  // Check if we should compact
61
102
  const shouldCompact = this.conversationHistory.length >= 40 &&
62
103
  (Date.now() - this.lastCompaction > 10 * 60 * 1000);
@@ -91,6 +132,10 @@ export class MCPClientOpenAI {
91
132
  let outOfToolCalls = false;
92
133
  while (loopCount < maxLoops) {
93
134
  loopCount++;
135
+ // Check for cancellation before each API call
136
+ if (abortSignal?.aborted) {
137
+ throw new Error("Request was cancelled");
138
+ }
94
139
  // Call OpenAI Responses API with error handling
95
140
  let response;
96
141
  try {
@@ -140,6 +185,10 @@ export class MCPClientOpenAI {
140
185
  if (functionCalls.length > 0) {
141
186
  this.conversationHistory.push(...output);
142
187
  for (const functionCall of functionCalls) {
188
+ // Check for cancellation before each tool call
189
+ if (abortSignal?.aborted) {
190
+ throw new Error("Request was cancelled");
191
+ }
143
192
  const functionName = functionCall.name;
144
193
  const functionArgs = typeof functionCall.arguments === 'string'
145
194
  ? JSON.parse(functionCall.arguments)
@@ -217,7 +266,9 @@ export class MCPClientOpenAI {
217
266
  return finalResponse;
218
267
  }
219
268
  clearHistory() {
220
- this.conversationHistory = [];
269
+ // Keep the system message (first item) when clearing history
270
+ const systemMessage = this.conversationHistory[0];
271
+ this.conversationHistory = systemMessage ? [systemMessage] : [];
221
272
  }
222
273
  async cleanup() {
223
274
  await this.client.close();
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.7",
4
4
  "description": "Reusable MCP client component with AI chat interface",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",