Skip to content
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

Add cross spawn and the test for it #198

Merged
merged 3 commits into from
Mar 26, 2025

Conversation

zhibisora
Copy link

@zhibisora zhibisora commented Mar 15, 2025

Replace Node.js native spawn with cross-spawn

Motivation and Context

This change improves cross-platform compatibility, particularly on Windows systems. The native spawn from Node.js can have issues with argument escaping, path handling, and environment variables on Windows. Using cross-spawn addresses these issues and provides a more consistent experience across different operating systems.

This is a fork of #95, which fixed #101. I've added tests to it.

How Has This Been Tested?

Added unit tests that specifically verify cross-spawn integration:

  • Test that cross-spawn is called with the correct parameters
  • Test environment variable passing
  • Test message sending functionality

All existing and new tests pass locally.

Breaking Changes

No breaking changes. This is a transparent replacement that maintains the same API and behavior.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The replacement is minimal and focused on the spawn implementation only. No changes were made to the stdio transport's behavior or API. This is a common pattern in Node.js packages to ensure cross-platform compatibility.

@zhibisora
Copy link
Author

zhibisora commented Mar 24, 2025

Can someone review this?

@zhibisora zhibisora mentioned this pull request Mar 24, 2025
9 tasks
Copy link
Member

@jspahrsummers jspahrsummers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for opening this PR! Can you please describe any manual testing steps you've followed to verify this works on Windows?

@zhibisora
Copy link
Author

zhibisora commented Mar 25, 2025

This is the client code for testing.

import { Anthropic } from "@anthropic-ai/sdk";
import type { MessageParam } from "@anthropic-ai/sdk/resources/messages";

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import readline from "readline/promises";

import dotenv from "dotenv";

dotenv.config(); // load environment variables from .env

const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (!ANTHROPIC_API_KEY) {
  throw new Error("ANTHROPIC_API_KEY is not set");
}

const MCP_SERVERS = {
  filesystem: {
    command: "npx",
    args: ["-y", "@modelcontextprotocol/server-filesystem", "C:\\Users\\sora"],
  },
  playwright: {
    command: "npx",
    args: ["-y", "@executeautomation/playwright-mcp-server"],
  },
};

interface Tool {
  name: string;
  description?: string;
  input_schema: {
    properties?: Record<string, unknown>;
    required?: string[];
  };
}

class MCPClient {
  private mcp: Client;
  private anthropic: Anthropic;
  private transport: StdioClientTransport | null = null;
  private tools: Tool[] = [];

  constructor() {
    // Initialize Anthropic client and MCP client
    this.anthropic = new Anthropic({
      apiKey: ANTHROPIC_API_KEY,
    });
    this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
  }

  async connectToServer(serverName: string) {
    /**
     * Connect to an MCP server using configuration
     *
     * @param serverName - Name of the server in config
     */
    try {
      const serverConfig = MCP_SERVERS[serverName as keyof typeof MCP_SERVERS];
      if (!serverConfig) {
        throw new Error(`Server "${serverName}" not found in config`);
      }

      // Initialize transport and connect to server
      this.transport = new StdioClientTransport({
        command: serverConfig.command,
        args: serverConfig.args,
      });
      this.mcp.connect(this.transport);

      // List available tools
      const toolsResult = await this.mcp.listTools();
      this.tools = toolsResult.tools.map((tool) => {
        return {
          name: tool.name,
          description: tool.description,
          input_schema: {
            properties: tool.inputSchema.properties,
            required: Array.isArray(tool.inputSchema.required) ? tool.inputSchema.required : [],
          },
        };
      });
      console.log(
        "Connected to server with tools:",
        this.tools.map(({ name }) => name),
      );
    } catch (e) {
      console.log("Failed to connect to MCP server: ", e);
      throw e;
    }
  }

  async processQuery(query: string) {
    /**
     * Process a query using Claude and available tools
     *
     * @param query - The user's input query
     * @returns Processed response as a string
     */
    const messages: MessageParam[] = [
      {
        role: "user",
        content: query,
      },
    ];

    // Initial Claude API call
    const response = await this.anthropic.messages.create({
      model: "claude-3-sonnet-20240229",
      max_tokens: 1000,
      messages,
      system: "You have access to external tools. Use them when appropriate.",
    });

    // Process response and handle tool calls
    const finalText = [];
    const toolResults = [];

    for (const content of response.content) {
      if (content.type === "text") {
        finalText.push(content.text);
      } else {
      
        console.log("Non-text content type encountered:", content.type);
      }
    }

    return finalText.join("\n");
  }

  async chatLoop() {
    /**
     * Run an interactive chat loop
     */
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
    });

    try {
      console.log("\nMCP Client Started!");
      console.log("Type your queries or 'quit' to exit.");

      while (true) {
        const message = await rl.question("\nQuery: ");
        if (message.toLowerCase() === "quit") {
          break;
        }
        const response = await this.processQuery(message);
        console.log("\n" + response);
      }
    } finally {
      rl.close();
    }
  }

  async cleanup() {
    /**
     * Clean up resources
     */
    await this.mcp.close();
  }
}

async function main() {
  const mcpClient = new MCPClient();
  
  try {
    await mcpClient.connectToServer("filesystem"); //  filesystem server
    await mcpClient.chatLoop();
  } finally {
    await mcpClient.cleanup();
    process.exit(0);
  }
}

main();

Testing with filesystem mcp.

Result:

截屏2025-03-25 21 37 24

I use npm link to use my local add-cross-spawn branch of the sdk.

It works well.

And without add-cross-spwan branch, it looks like:

截屏2025-03-25 21 44 45

So I think it's effective.

@jspahrsummers
Copy link
Member

Great, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Make SDK work on Windows without additional arguments
2 participants