-
-
Notifications
You must be signed in to change notification settings - Fork 7
fix: initialize SkyFi MCP before connection status check #770
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,8 @@ import { SkyfiOAuthProvider } from '@/lib/skyfi/provider'; | |
| import crypto from 'crypto'; | ||
|
|
||
| import { headers } from 'next/headers'; | ||
| import { Client as MCPClient } from '@modelcontextprotocol/sdk/client/index.js'; | ||
| import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; | ||
|
|
||
| export async function getRedirectUri(): Promise<string> { | ||
| try { | ||
|
|
@@ -143,38 +145,39 @@ export async function getSkyfiConnectionStatus(): Promise<{ connected: boolean; | |
| const timeoutId = setTimeout(() => controller.abort(), 10000); | ||
|
|
||
| try { | ||
| // Try a simple whoami call to verify token validity and get email/budget | ||
| const res = await fetch('https://mcp.skyfi.com/mcp', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'Authorization': `Bearer ${tokens.access_token}`, | ||
| }, | ||
| body: JSON.stringify({ | ||
| jsonrpc: '2.0', | ||
| id: 1, | ||
| method: 'tools/call', | ||
| params: { | ||
| name: 'skyfi_whoami', | ||
| arguments: {}, | ||
| // Use the MCP SDK rather than calling tools/call directly. Streamable HTTP | ||
| // servers require an initialize handshake (and may issue a session ID) | ||
| // before tool calls are accepted. | ||
| const transport = new StreamableHTTPClientTransport( | ||
| new URL('https://mcp.skyfi.com/mcp'), | ||
| { | ||
| requestInit: { | ||
| headers: { Authorization: `Bearer ${tokens.access_token}` }, | ||
| signal: controller.signal, | ||
| }, | ||
| }), | ||
| signal: controller.signal, | ||
| }); | ||
|
|
||
| clearTimeout(timeoutId); | ||
|
|
||
| if (res.ok) { | ||
| const data = await res.json(); | ||
| const content = data?.result?.content?.[0]?.text || ''; | ||
| }, | ||
| ); | ||
| const client = new MCPClient({ name: 'QCXSkyFiStatus', version: '1.0.0' }); | ||
|
|
||
| try { | ||
| await client.connect(transport); | ||
| const result = await client.callTool( | ||
| { name: 'skyfi_whoami', arguments: {} }, | ||
| undefined, | ||
| { signal: controller.signal }, | ||
| ); | ||
| const content = (result as any)?.content?.[0]?.text || ''; | ||
| clearTimeout(timeoutId); | ||
| return { connected: true, budget: content }; | ||
| } finally { | ||
| await client.close().catch(() => undefined); | ||
|
Comment on lines
+170
to
+173
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Cleanup bypasses status timeout After a successful tool call, getSkyfiConnectionStatus clears its 10-second timer before its finally block awaits client.close(), so a stalled close leaves the settings status request and loading UI pending indefinitely. The same unbounded close also delays timeout failures because the catch cannot run until the inner finally completes. Agent Prompt
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: printf '%s\n' '--- changed file ---'
sed -n '1,220p' lib/actions/skyfi.ts
printf '%s\n' '--- dependency declarations ---'
rg -n '"`@modelcontextprotocol/sdk`"|modelcontextprotocol' package.json package-lock.json npm-shrinkwrap.json yarn.lock pnpm-lock.yaml 2>/dev/null || true
printf '%s\n' '--- local SDK sources, if present ---'
fd -i 'StreamableHTTPClientTransport|Client' . --type f 2>/dev/null | head -80Repository: QueueLab/QCX Length of output: 8128 🌐 Web query:
💡 Result: In the Model Context Protocol (MCP) TypeScript SDK, specifically for the StreamableHTTPClientTransport, the relationship between terminateSession and client.close is as follows: 1. terminateSession: This method is used to explicitly inform the server that a session is no longer needed [1][2]. It sends an HTTP DELETE request to the MCP endpoint, including the Mcp-Session-Id header [1][3][2]. If supported, this allows the server to clean up resources associated with that session ID [3][4]. It is a recommended practice to call this before closing the client if the server supports session termination [4]. 2. close: This method is responsible for the local cleanup of the transport connection [5][6]. Its implementation focuses on local resource management: it cancels any pending reconnection attempts, aborts active HTTP requests (via an AbortController), and invokes the onclose callback [5][6]. Crucially, the close method does not automatically trigger the terminateSession HTTP DELETE call [3][4]. Therefore, to properly shut down a connection to a Streamable HTTP server, the recommended pattern is to first await transport.terminateSession to inform the server, followed by transport.close to clean up local resources [4]. Note that some servers may respond with a 405 Method Not Allowed to the DELETE request if they do not support explicit session termination, in which case the transport remains valid for closing locally [1][4][2]. Citations:
🌐 Web query:
💡 Result: In the Model Context Protocol (MCP) TypeScript SDK, the Citations:
Terminate the Streamable HTTP session.
🤖 Prompt for AI Agents |
||
| } | ||
| } catch (fetchError) { | ||
| clearTimeout(timeoutId); | ||
| console.warn('[SkyFiAction: getSkyfiConnectionStatus] Failed to query whoami:', fetchError); | ||
| console.warn('[SkyfiAction: getSkyfiConnectionStatus] Failed to query whoami:', fetchError); | ||
| } | ||
|
|
||
| return { connected: true }; | ||
| return { connected: false }; | ||
| } catch (error: any) { | ||
| console.error('[SkyFiAction: getSkyfiConnectionStatus] Error:', error.message); | ||
| return { connected: false }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: QueueLab/QCX
Length of output: 7967
🌐 Web query:
@modelcontextprotocol/sdk 1.13.0 Client callTool CallToolResult isError TypeScript contract💡 Result:
In the Model Context Protocol (MCP) TypeScript SDK, including version 1.13.0, the callTool method is used to invoke a tool on an MCP server [1][2][3]. The result returned by this method includes an isError field, which is critical for handling tool execution outcomes correctly [2][4][3]. Key aspects of the CallToolResult and isError contract: 1. isError usage: The isError boolean flag distinguishes between successful tool execution and handled application-level errors [4][5][6]. When a tool execution fails (e.g., due to invalid arguments, business logic errors, or API failures), the handler should return a result with isError: true [1][4][5]. This allows the connected Large Language Model (LLM) to read the error message provided in the content field and potentially self-correct in subsequent requests [4][7][5]. 2. Error vs. Protocol Failure: - Tool Error: If a tool handler returns isError: true, or if it throws an exception (which the SDK automatically catches and converts to an isError: true response), it is treated as a successful JSON-RPC result that the model can interpret [4][7]. - Protocol-level Failure: Only severe issues that prevent the tool call from being processed at the protocol level—such as calling a tool name that is not registered or experiencing a network timeout—will result in an actual JSON-RPC error (i.e., throwing an exception out of the callTool method) [2][4][7]. 3. Input Validation: The SDK automatically validates arguments against the tool's inputSchema [8][9]. If validation fails, the SDK rejects the call before the handler runs, returning a result with isError: true, which again allows the model to see the error and retry with corrected parameters [8][10][9]. 4. Content: The content field is an array of content blocks (such as text, images, or resources) [7][8][9]. When isError is true, this field should contain a descriptive error message that assists the model in understanding the failure [4][5][6]. In summary, the TypeScript contract dictates that clients should always check the isError property on a CallToolResult before relying on the content, as a failed tool call is returned as a valid object rather than a thrown exception [2][3].
Citations:
Return disconnected for MCP tool errors. Check
result.isErrorbefore returning{ connected: true };Client.callToolreturns tool-level failures asCallToolResultobjects, so a failedskyfi_whoamicall can otherwise report a connected account.🤖 Prompt for AI Agents