Skip to content

Draft: Basic MCP implementation #4982

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

Open
wants to merge 1 commit into
base: 3.x
Choose a base branch
from
Open

Draft: Basic MCP implementation #4982

wants to merge 1 commit into from

Conversation

DavertMik
Copy link
Contributor

  • list tests & suites
  • list all available actions
  • tool to open and close browser
  • ability to control browser via actions
  • tool to collect HTML / page info
  • tool for screenshot

@kobenguyent kobenguyent requested a review from Copilot April 29, 2025 08:08
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces documentation for integrating CodeceptJS with Cursor using the Model Context Protocol (MCP), outlining how to start the MCP server and utilize its available tools.

  • Added a dedicated markdown file (docs/MCP.md) detailing MCP usage with Cursor
  • Provided step-by-step instructions for starting the MCP server and connecting via Cursor
  • Outlined available tools and included troubleshooting guidance for common issues
Files not reviewed (1)
  • package.json: Language not supported


The server provides these tools:

- `list-tests`: List all availble tests
Copy link
Preview

Copilot AI Apr 29, 2025

Choose a reason for hiding this comment

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

Typo detected in the word 'availble'; please update it to 'available'.

Suggested change
- `list-tests`: List all availble tests
- `list-tests`: List all available tests

Copilot uses AI. Check for mistakes.

Copy link
Collaborator

@kobenguyent kobenguyent left a comment

Choose a reason for hiding this comment

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

What do you think if we do like this to keep the server.tool clean and reusable?

@@ -0,0 +1,185 @@
#!/usr/bin/env node
Copy link
Collaborator

Choose a reason for hiding this comment

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

#!/usr/bin/env node

const { McpServer, ResourceTemplate } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');

// Import specific CodeceptJS modules directly
const Codecept = require('../lib/codecept');
const container = require('../lib/container');
const { getParamsToString } = require('../lib/parser');
const { methodsOfObject } = require('../lib/utils');
const output = require('../lib/output');
const { getConfig, getTestRoot } = require('../lib/command/utils');

// Determine the project path
const projectPath = process.argv.slice(2).find(arg => !arg.includes('mcp.js')) || process.cwd();

/**

  • Initializes CodeceptJS and loads tests.
  • @param {string} customPath - The project path.
  • @returns {object} - The initialized CodeceptJS instance and its configuration.
    */
    async function initializeCodeceptJS(customPath) {
    output.print = () => {}; // Disable default output
    const testsPath = getTestRoot(customPath);
    const config = getConfig(customPath);
    const codecept = new Codecept(config, {});
    await codecept.init(testsPath);
    codecept.loadTests();
    return { codecept, config };
    }

/**

  • Extracts test information from the loaded CodeceptJS instance.
  • @param {object} codecept - The initialized CodeceptJS instance.
  • @returns {Array} - An array of test objects.
    */
    function extractTestInfo(codecept) {
    const mocha = container.mocha();
    mocha.files = codecept.testFiles;
    mocha.loadFiles();

    const tests = [];
    for (const suite of mocha.suite.suites) {
    for (const test of suite.tests) {
    tests.push({
    title: test.title,
    fullTitle: test.fullTitle(),
    body: test.body ? test.body.toString() : '',
    file: suite.file,
    suiteName: suite.title,
    meta: test.meta,
    tags: test.tags,
    });
    }
    }
    return tests;
    }

    /**

    • Formats test information into a readable text block.
    • @param {Array} tests - An array of test objects.
    • @returns {object} - An MCP content object containing the formatted text.
      */
      function formatTestsAsText(tests) {
      const formattedText = tests
      .map(test => [
      Test: ${test.fullTitle},
      File: ${test.file},
      Suite: ${test.suiteName},
      test.tags && test.tags.length ? Tags: ${test.tags.join(', ')} : '',
      '',
      'Body:',
      test.body,
      '---',
      ]
      .filter(Boolean)
      .join('\n'))
      .join('\n\n');
    • return { content: [{ type: 'text', text: formattedText }] };
      }

      /**

      • Extracts suite information from the loaded CodeceptJS instance.
      • @param {object} codecept - The initialized CodeceptJS instance.
      • @returns {Array} - An array of suite objects.
        */
        function extractSuiteInfo(codecept) {
        const mocha = container.mocha();
        mocha.files = codecept.testFiles;
        mocha.loadFiles();

        const suites = [];
        for (const suite of mocha.suite.suites) {
        suites.push({
        title: suite.title,
        file: suite.file,
        testCount: suite.tests.length,
        tests: suite.tests.map(test => ({
        title: test.title,
        fullTitle: test.fullTitle(),
        })),
        });
        }
        return suites;
        }

        /**

        • Formats suite information into a readable text block.
        • @param {Array} suites - An array of suite objects.
        • @returns {object} - An MCP content object containing the formatted text.
          */
          function formatSuitesAsText(suites) {
          const formattedText = suites
          .map(suite => {
          const testList = suite.tests.map(test => - ${test.title}).join('\n');
          return [Suite: ${suite.title}, File: ${suite.file}, Tests (${suite.testCount}):, testList, '---'].join('\n');
          })
          .join('\n\n');
        • return { content: [{ type: 'text', text: formattedText }] };
          }

          /**

          • Extracts action information from CodeceptJS helpers and support objects.
          • @returns {Array} - An array of action objects.
            */
            function extractActionInfo() {
            const helpers = container.helpers();
            const supportI = container.support('I');
            const actions = [];

            // Get actions from helpers
            for (const name in helpers) {
            const helper = helpers[name];
            methodsOfObject(helper).forEach(action => {
            const params = getParamsToString(helper[action]);
            actions.push({
            name: action,
            source: name,
            params,
            type: 'helper',
            });
            });
            }

            // Get actions from I
            for (const name in supportI) {
            if (actions.some(a => a.name === name)) {
            continue;
            }
            const actor = supportI[name];
            const params = getParamsToString(actor);
            actions.push({
            name,
            source: 'I',
            params,
            type: 'support',
            });
            }
            return actions;
            }

            /**

            • Formats action information into a readable text block.
            • @param {Array} actions - An array of action objects.
            • @returns {object} - An MCP content object containing the formatted text.
              */
              function formatActionsAsText(actions) {
              const helperActions = actions
              .filter(a => a.type === 'helper')
              .sort((a, b) => (a.source === b.source ? a.name.localeCompare(b.name) : a.source.localeCompare(b.source)));
            • const supportActions = actions.filter(a => a.type === 'support').sort((a, b) => a.name.localeCompare(b.name));

              const formattedText = [
              '# Helper Actions',
              ...helperActions.map(a => ${a.source}.${a.name}(${a.params})),
              '',
              '# Support Actions',
              ...supportActions.map(a => I.${a.name}(${a.params})),
              ].join('\n');

              return { content: [{ type: 'text', text: formattedText }] };
              }

              /**

              • Starts the MCP Server.
                */
                async function startServer() {
                const { codecept } = await initializeCodeceptJS(projectPath);

              const server = new McpServer({
              name: 'CodeceptJS',
              version: '1.0.0',
              url: 'https://codecept.io',
              description: 'CodeceptJS Model Context Protocol Server',
              });

              server.tool('list-tests', {}, async () => {
              const tests = extractTestInfo(codecept);
              return formatTestsAsText(tests);
              });

              server.tool('list-suites', {}, async () => {
              const suites = extractSuiteInfo(codecept);
              return formatSuitesAsText(suites);
              });

              server.tool('list-actions', {}, async () => {
              const actions = extractActionInfo();
              return formatActionsAsText(actions);
              });

              const transport = new StdioServerTransport();
              await server.connect(transport);
              }

              // Start the server
              startServer().catch(err => {
              console.error('Error starting MCP server:', err);
              process.exit(1);
              });

// Setup MCP server
const server = new McpServer({
name: 'CodeceptJS',
version: '1.0.0',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shall we get the current version of codeceptjs?

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.

2 participants