diff --git a/.gitignore b/.gitignore index 501f85a22f..34f003ebbd 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ test.txt *.log *.vsix **/.vscode/CMakeTools +.vscode/mcp.json **/nls.*.json **/*.nls.json **/*.nls.*.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 10bf5f1540..1c1cabe67e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,22 @@ This article is for developers who want to contribute to the CMake Tools open source project. +## New here? Try the onboarding assistant + +This repo includes an MCP server that gives GitHub Copilot structured knowledge about the codebase. It can walk you through setup, explain concepts like kits and presets, point you to the right source files, and help you find issues to work on. + +To set it up, build it once: + +```bash +cd tools/onboarding-mcp +yarn install +yarn build +``` + +Then open **Copilot Chat** in VS Code (`Ctrl+Shift+I`), switch to **Agent mode**, and ask anything — for example *"How do I set up this repo?"* or *"What's a good first issue?"* + +See [`tools/onboarding-mcp/README.md`](tools/onboarding-mcp/README.md) for full details and example prompts. + ## Build the CMake Tools extension As with most VS Code extensions, you'll need [Node.JS](https://nodejs.org/en/) installed. We use yarn to compile the code (run `npm install -g yarn` to install it). diff --git a/tools/onboarding-mcp/.eslintignore b/tools/onboarding-mcp/.eslintignore new file mode 100644 index 0000000000..1eae0cf670 --- /dev/null +++ b/tools/onboarding-mcp/.eslintignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/tools/onboarding-mcp/.eslintrc.cjs b/tools/onboarding-mcp/.eslintrc.cjs new file mode 100644 index 0000000000..cb188da7ba --- /dev/null +++ b/tools/onboarding-mcp/.eslintrc.cjs @@ -0,0 +1,18 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + sourceType: "module" + }, + plugins: ["@typescript-eslint"], + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + rules: { + // Keep it simple — this is a small standalone tool + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }] + } +}; diff --git a/tools/onboarding-mcp/README.md b/tools/onboarding-mcp/README.md new file mode 100644 index 0000000000..d6ca5783b7 --- /dev/null +++ b/tools/onboarding-mcp/README.md @@ -0,0 +1,101 @@ +# CMake Tools Onboarding MCP Server + +A [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that helps new contributors onboard to the [vscode-cmake-tools](https://github.com/microsoft/vscode-cmake-tools) extension. It provides Copilot agent mode with structured, repo-specific knowledge about setup, code structure, and PR requirements. + +## Install and build + +```bash +cd tools/onboarding-mcp +yarn install +yarn build +``` + +## Wire it up in VS Code + +Create (or add to) **`.vscode/mcp.json`** at the workspace root: + +```json +{ + "servers": { + "cmake-tools-onboarding": { + "type": "stdio", + "command": "node", + "args": ["${workspaceFolder}/tools/onboarding-mcp/dist/index.js"], + "env": { + "GITHUB_TOKEN": "${env:GITHUB_TOKEN}" + } + } + } +} +``` + +The `GITHUB_TOKEN` env var is **optional** but recommended. Without it, GitHub API calls are limited to 60 requests/hour. With a token, the limit is 5,000/hour. You can create a personal access token at https://github.com/settings/tokens — no scopes are needed for public repos. + +Once configured, the MCP server is available to Copilot agent mode (and any other MCP client) in VS Code. + +## Usage + +The MCP server works **inside GitHub Copilot Chat** in VS Code. After building and wiring it up (see [above](#wire-it-up-in-vs-code)), just open Copilot Chat and go: + +```bash +cd tools/onboarding-mcp +yarn install +yarn build +``` + +Then open a **Copilot Chat** panel (press `Ctrl+Shift+I` or click the Copilot icon in the sidebar) and switch to **Agent mode** (the dropdown at the top of the chat panel). VS Code may show a notification asking you to start the MCP server — click **Start**. + +Once it's running, just ask questions in natural language. Copilot will automatically call the right tools. Here are some things you can try: + +### Getting started +- *"How do I set up this repo for local development?"* +- *"Walk me through building and running the extension."* + +### Understanding the codebase +- *"What is a kit? How is it different from a preset?"* +- *"Explain how the CMake driver works."* +- *"Where is the code that handles CTest?"* +- *"Which source file should I look at for build logic?"* + +### Finding documentation +- *"Show me the docs page about CMake Presets."* +- *"Where can I find troubleshooting info?"* + +### Exploring issues to work on +- *"What are some good issues for a new contributor?"* +- *"Show me recent open issues labeled 'enhancement'."* + +### Understanding recent activity +- *"What areas of the codebase have changed recently?"* +- *"Show me the last 5 commits to main."* + +### Preparing a PR +- *"I changed the preset loading logic and added a new setting. Is my PR ready?"* +- *"What do I need to check before submitting a PR?"* + +> **Tip:** You don't need to name the tools or remember their inputs. Just describe what you need and Copilot will figure out which tool to call. + +## Development + +```bash +# Run the server in dev mode (uses tsx, no build step needed): +yarn dev + +# Build for production: +yarn build + +# Run the production build: +yarn start +``` + +## Available tools + +| Tool | Input | Description | +| --- | --- | --- | +| **`get_setup_guide`** | _(none)_ | Returns an ordered list of setup steps for new contributors to build and run the extension locally. Each step includes a number, title, optional shell command, and optional notes. | +| **`check_pr_readiness`** | `{ "changes": "..." }` | Given a free-text description of changes, returns a checklist of PR requirements from CONTRIBUTING.md. Each item includes the rule, a status (`"pass"`, `"warn"`, or `"manual_check_required"`), and a helpful hint. Items are flagged as `"warn"` when the description suggests a potential issue (e.g. mentioning `package.json` changes). | +| **`explain_concept`** | `{ "concept": "..." }` | Explains a CMake Tools concept (e.g. `kit`, `preset`, `driver`, `ctest`, `build`, `configure`, `debug`, `settings`). Returns a summary, detailed explanation, related concepts, relevant source files, and a link to the docs page. If the concept is unknown, lists all known concepts. | +| **`find_source_file`** | `{ "feature": "..." }` | Given a natural-language description (e.g. `"kit scanning"`, `"build logic"`, `"test runner"`), returns matching source files with GitHub links, descriptions, and relevance notes. Useful for quickly navigating to the right file. | +| **`get_docs_page`** | `{ "topic": "..." }` | Given a topic (e.g. `presets`, `kits`, `debugging`, `troubleshooting`, `faq`), returns the matching documentation page with file path, GitHub URL, summary, and key section headings. | +| **`get_contributor_issues`** | `{ "limit?": 20, "label?": "..." }` | Fetches recently updated open issues and enriches each with contributor-friendliness signals. Optionally filter by label (e.g. `"bug"`, `"enhancement"`, `"good first issue"`, `"help wanted"`). Requires a network connection; set `GITHUB_TOKEN` for higher rate limits (see [above](#wire-it-up-in-vs-code)). | +| **`get_recent_changes`** | `{ "limit?": 10 }` | Fetches the most recent commits to main and annotates each with `affectedAreas` derived from the codebase source map. Includes a summary of the most active areas so new contributors can see what's currently in flux. | diff --git a/tools/onboarding-mcp/package.json b/tools/onboarding-mcp/package.json new file mode 100644 index 0000000000..1aa7c1fd17 --- /dev/null +++ b/tools/onboarding-mcp/package.json @@ -0,0 +1,24 @@ +{ + "name": "cmake-tools-onboarding-mcp", + "version": "1.0.0", + "private": true, + "description": "MCP server to help new contributors onboard to microsoft/vscode-cmake-tools", + "type": "module", + "engines": { + "node": ">=18" + }, + "scripts": { + "build": "tsc", + "dev": "tsx src/index.ts", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.27.1", + "zod": "^3.25.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } +} diff --git a/tools/onboarding-mcp/src/data/concepts.ts b/tools/onboarding-mcp/src/data/concepts.ts new file mode 100644 index 0000000000..aedd05faf5 --- /dev/null +++ b/tools/onboarding-mcp/src/data/concepts.ts @@ -0,0 +1,282 @@ +const GITHUB_BASE = "https://github.com/microsoft/vscode-cmake-tools/blob/main"; + +export interface ConceptEntry { + concept: string; + aliases: string[]; + summary: string; + details: string; + relatedConcepts: string[]; + sourceFiles: string[]; + docsPage: string; + docsUrl: string; +} + +export const concepts: ConceptEntry[] = [ + { + concept: "kit", + aliases: ["kits", "compiler kit", "toolchain kit"], + summary: + "A kit describes the compiler toolchain used to build a CMake project — the compiler, target architecture, and optional toolchain file. " + + "Kits are scanned automatically or defined manually.", + details: + "Kits are the legacy (non-presets) way of telling CMake Tools which compiler to use. " + + "On Windows, MSVC kits require the VS Developer Environment (vcvarsall.bat) to be merged into the build environment. " + + "Kits are stored in a user-local cmake-kits.json file or a project-local .vscode/cmake-kits.json. " + + "The extension can auto-scan for available compilers via the 'CMake: Scan for Kits' command. " + + "If the project uses CMakePresets.json, kits are not used — presets take priority.", + relatedConcepts: ["variant", "preset", "configure", "intellisense"], + sourceFiles: ["src/kits/kit.ts", "src/kits/kitsController.ts"], + docsPage: "docs/kits.md", + docsUrl: `${GITHUB_BASE}/docs/kits.md` + }, + { + concept: "variant", + aliases: ["variants", "build variant", "cmake-variants.yaml"], + summary: + "Variants let you define a matrix of build configurations (e.g. Debug/Release + shared/static). " + + "They are defined in a cmake-variants.yaml file and are an alternative to CMake Presets for simpler projects.", + details: + "Variants are the legacy way (before CMake Presets) of selecting build type and other CMAKE_* cache variables. " + + "A cmake-variants.yaml (or .json) file in the project or .vscode folder defines named settings with choices. " + + "The user picks a combination via the status bar. Variants are not used when CMakePresets.json is present. " + + "The variant schema supports buildType, linkage, and arbitrary CMake cache variables.", + relatedConcepts: ["kit", "preset", "configure", "build"], + sourceFiles: ["src/kits/variant.ts"], + docsPage: "docs/variants.md", + docsUrl: `${GITHUB_BASE}/docs/variants.md` + }, + { + concept: "preset", + aliases: ["presets", "cmake preset", "cmakepresets", "cmakepresets.json", "cmake presets"], + summary: + "CMake Presets (CMakePresets.json) are the modern, standardized way to define configure, build, and test settings. " + + "The extension supports CMakePresets.json and CMakeUserPresets.json, including inheritance and environment variables.", + details: + "CMakePresets.json is project-owned (committed to source control). CMakeUserPresets.json is user-owned (gitignored). " + + "Both support 'include' chaining. The merged preset tree lives in PresetsController — never re-parse preset files directly. " + + "Preset types include configure, build, test, package, and workflow presets. " + + "When presets are enabled, kits and variants are ignored. " + + "The extension detects the presets file version and validates against the appropriate JSON schema.", + relatedConcepts: ["kit", "variant", "configure", "build", "ctest"], + sourceFiles: ["src/presets/preset.ts", "src/presets/presetsController.ts", "src/presets/presetsParser.ts"], + docsPage: "docs/cmake-presets.md", + docsUrl: `${GITHUB_BASE}/docs/cmake-presets.md` + }, + { + concept: "driver", + aliases: ["drivers", "cmake driver", "cmake process"], + summary: + "A driver is the internal abstraction that communicates with the CMake process. " + + "There are two drivers: the CMake File API driver (modern, default) and the legacy server driver.", + details: + "The abstract base class CMakeDriver in src/drivers/cmakeDriver.ts defines the interface for configure, build, and target queries. " + + "The File API driver (cmakeFileApiDriver.ts) writes query files and reads reply files from .cmake/api/v1/ — this is the modern default. " + + "The legacy server driver (cmakeServerDriver.ts) uses the deprecated cmake-server protocol. " + + "Most contributors only need to modify cmakeDriver.ts (shared logic) or cmakeFileApiDriver.ts. " + + "The driver produces a CodeModelContent (defined in codeModel.ts) after configure — the authoritative source for targets and file groups.", + relatedConcepts: ["configure", "build", "ctest", "extension"], + sourceFiles: [ + "src/drivers/cmakeDriver.ts", + "src/drivers/cmakeFileApiDriver.ts", + "src/drivers/cmakeLegacyDriver.ts", + "src/drivers/drivers.ts" + ], + docsPage: "docs/configure.md", + docsUrl: `${GITHUB_BASE}/docs/configure.md` + }, + { + concept: "ctest", + aliases: ["test", "tests", "test runner", "test explorer"], + summary: + "CTest is CMake's test runner. The extension integrates CTest results into the VS Code Test Explorer, " + + "allowing you to run, debug, and view test results directly in the editor.", + details: + "src/ctest.ts is one of the largest files in the repo. It parses CTest output, maps tests to the VS Code Test Explorer API, " + + "and supports test filtering, re-running failed tests, and debugging individual tests. " + + "Test presets (in CMakePresets.json) or the cmake.ctest.* settings control CTest behavior. " + + "The CTest driver is separate from the build driver — it has its own preset type and execution logic.", + relatedConcepts: ["preset", "configure", "build", "debug"], + sourceFiles: ["src/ctest.ts"], + docsPage: "docs/debug-launch.md", + docsUrl: `${GITHUB_BASE}/docs/debug-launch.md` + }, + { + concept: "configure", + aliases: ["configuration", "cmake configure", "configure step"], + summary: + "The configure step runs cmake to generate build files. It is triggered automatically on first open or manually via commands. " + + "Configuration state is tracked in src/cmakeProject.ts.", + details: + "Configuring runs the CMake command with the appropriate generator, toolchain, and cache variables. " + + "In kit/variant mode, CMAKE_BUILD_TYPE is set at configure time for single-config generators. " + + "In presets mode, the configure preset defines all these settings. " + + "A 'clean configure' deletes the build directory and re-runs CMake from scratch. " + + "The configure step also supports the CMake Debugger, which lets you step through CMakeLists.txt line by line. " + + "After configure, the driver reads the code model to learn about targets and source files.", + relatedConcepts: ["build", "driver", "preset", "kit", "variant"], + sourceFiles: ["src/cmakeProject.ts", "src/drivers/cmakeDriver.ts"], + docsPage: "docs/configure.md", + docsUrl: `${GITHUB_BASE}/docs/configure.md` + }, + { + concept: "build", + aliases: ["compile", "build step", "cmake build"], + summary: + "The build step compiles the project using the selected kit/preset. " + + "The build runner is in src/cmakeBuildRunner.ts; the full project orchestration is in src/cmakeProject.ts.", + details: + "Building invokes cmake --build with the correct build directory and configuration. " + + "For multi-config generators (Visual Studio, Ninja Multi-Config), the --config flag selects the build type at build time. " + + "For single-config generators (Ninja, Unix Makefiles), the build type was already set during configure. " + + "The build runner streams output to the terminal and parses diagnostics (errors/warnings) per compiler family — " + + "see src/diagnostics/ for GCC, MSVC, GHS, IAR, and other parsers. " + + "Build tasks can also be defined in tasks.json via the CMake task provider.", + relatedConcepts: ["configure", "driver", "task", "preset", "kit"], + sourceFiles: ["src/cmakeBuildRunner.ts", "src/cmakeProject.ts"], + docsPage: "docs/build.md", + docsUrl: `${GITHUB_BASE}/docs/build.md` + }, + { + concept: "task", + aliases: ["tasks", "vscode task", "cmake task", "task provider"], + summary: + "CMake Tools exposes VS Code tasks (configure, build, test, etc.) via src/cmakeTaskProvider.ts. " + + "Tasks allow contributors to wire CMake operations into VS Code's task system and terminal.", + details: + "The CMake task provider registers a 'cmake' task type. Users can define custom tasks in tasks.json " + + "that run configure, build, test, install, clean, or clean-rebuild operations. " + + "Tasks support the same presets and kit/variant settings as the command palette. " + + "This file also handles the build and configure task definitions used by the extension internally.", + relatedConcepts: ["build", "configure", "ctest"], + sourceFiles: ["src/cmakeTaskProvider.ts"], + docsPage: "docs/tasks.md", + docsUrl: `${GITHUB_BASE}/docs/tasks.md` + }, + { + concept: "intellisense", + aliases: ["intellisense", "code completion", "include paths", "compile commands"], + summary: + "IntelliSense integration passes compile commands and include paths to the C/C++ extension (ms-vscode.cpptools) " + + "via src/cpptools.ts. This enables accurate code completion and error squiggles for C/C++ files.", + details: + "After a successful configure, the code model contains per-file compiler flags, include paths, and defines. " + + "src/cpptools.ts implements the CppToolsApi configuration provider interface. " + + "src/compilationDatabase.ts handles compile_commands.json export for tools that use it. " + + "If IntelliSense is not working, the most common cause is a failed configure — the code model is only available after a successful configure.", + relatedConcepts: ["configure", "driver", "cpptools"], + sourceFiles: ["src/cpptools.ts", "src/compilationDatabase.ts"], + docsPage: "docs/how-to.md", + docsUrl: `${GITHUB_BASE}/docs/how-to.md` + }, + { + concept: "cpptools", + aliases: ["cpptools", "c/c++ extension", "ms-vscode.cpptools"], + summary: + "cpptools refers to the IntelliSense integration with the Microsoft C/C++ extension (ms-vscode.cpptools). " + + "CMake Tools provides compile flags and include paths so that IntelliSense works accurately.", + details: + "This is an alias for the 'intellisense' concept. " + + "src/cpptools.ts implements the CppToolsApi configuration provider. " + + "The configuration provider is registered once the C/C++ extension API is available. " + + "Changes here affect how include paths, defines, and compiler flags are reported to IntelliSense.", + relatedConcepts: ["intellisense", "configure"], + sourceFiles: ["src/cpptools.ts"], + docsPage: "docs/how-to.md", + docsUrl: `${GITHUB_BASE}/docs/how-to.md` + }, + { + concept: "debug", + aliases: ["debugging", "debugger", "launch", "debug target"], + summary: + "Debug support allows launching and debugging CMake targets directly from VS Code. " + + "Launch configuration is handled in src/debug/ and wired into launch.json support.", + details: + "CMake Tools supports 'quick debugging' (no launch.json needed) and traditional launch.json-based debugging. " + + "The user selects a launch target (an executable target from the code model), then runs 'CMake: Debug'. " + + "The extension also includes a CMake script debugger (for debugging CMakeLists.txt itself) in src/debug/cmakeDebugger/. " + + "For C/C++ debugging, the extension delegates to the cpptools debugger (cppdbg) or lldb-dap. " + + "CTest tests can also be debugged individually from the Test Explorer.", + relatedConcepts: ["build", "ctest", "configure", "extension"], + sourceFiles: ["src/debug/debugger.ts", "src/debug/cmakeDebugger/debugConfigurationProvider.ts"], + docsPage: "docs/debug-launch.md", + docsUrl: `${GITHUB_BASE}/docs/debug-launch.md` + }, + { + concept: "extension", + aliases: ["entry point", "extension.ts", "activation"], + summary: + "The extension entry point is src/extension.ts. It registers all commands, initializes the project controller, " + + "and wires together the UI, kits, presets, and driver subsystems.", + details: + "src/extension.ts is one of the largest files in the repo. It contains the ExtensionManager class which is the true singleton of the extension. " + + "It acts as glue between the lower layers (drivers, kits, presets) and the VS Code UX (commands, status bar, tree views). " + + "New contributors should read this file last — start with the subsystem relevant to your change. " + + "The project controller (src/projectController.ts) manages multi-folder workspaces and routes commands to the correct CMakeProject instance.", + relatedConcepts: ["driver", "kit", "preset", "settings"], + sourceFiles: ["src/extension.ts", "src/projectController.ts"], + docsPage: "docs/README.md", + docsUrl: `${GITHUB_BASE}/docs/README.md` + }, + { + concept: "settings", + aliases: ["config", "configuration settings", "cmake settings", "cmake-tools settings"], + summary: + "All user-facing extension settings are typed and read in src/config.ts. " + + "If you're adding a new setting, this is the file to modify alongside package.json.", + details: + "ConfigurationReader in src/config.ts is the canonical access point for all extension settings — " + + "never call vscode.workspace.getConfiguration() directly. " + + "When adding a new setting, you must update three locations: package.json (contributes.configuration), " + + "src/config.ts (ConfigurationReader), and docs/cmake-settings.md. " + + "Settings support variable substitution (${workspaceFolder}, etc.) via src/expand.ts.", + relatedConcepts: ["extension", "preset", "kit", "variant"], + sourceFiles: ["src/config.ts"], + docsPage: "docs/cmake-settings.md", + docsUrl: `${GITHUB_BASE}/docs/cmake-settings.md` + }, + { + concept: "cpack", + aliases: ["cpack", "packaging", "cmake package"], + summary: + "CPack is CMake's packaging tool. The extension integrates CPack to create distributable packages " + + "from built targets, with support for package presets.", + details: + "src/cpack.ts contains the CPack driver which runs cpack with the appropriate configuration. " + + "Package presets in CMakePresets.json control CPack behavior when in presets mode. " + + "CPack is invoked after a successful build to generate installers, archives, or other distributable formats.", + relatedConcepts: ["build", "preset", "configure"], + sourceFiles: ["src/cpack.ts"], + docsPage: "docs/cmake-presets.md", + docsUrl: `${GITHUB_BASE}/docs/cmake-presets.md` + }, + { + concept: "workflow", + aliases: ["workflow", "workflow preset"], + summary: + "Workflow presets chain configure, build, test, and package steps into a single automated sequence. " + + "They are defined in CMakePresets.json and executed via the 'CMake: Workflow' command.", + details: + "src/workflow.ts contains the workflow driver. A workflow preset references other presets in order " + + "(configure → build → test → package). This lets teams define end-to-end CI-like flows that contributors " + + "can run locally with a single command.", + relatedConcepts: ["preset", "configure", "build", "ctest", "cpack"], + sourceFiles: ["src/workflow.ts"], + docsPage: "docs/cmake-presets.md", + docsUrl: `${GITHUB_BASE}/docs/cmake-presets.md` + } +]; + +/** Get all known concept names for listing in error messages */ +export function knownConceptNames(): string[] { + return concepts.map((c) => c.concept); +} + +/** Find a concept by name or alias (case-insensitive) */ +export function findConcept(name: string): ConceptEntry | undefined { + const lower = name.toLowerCase().trim(); + return concepts.find( + (c) => + c.concept === lower || + c.aliases.some((a) => a.toLowerCase() === lower) + ); +} diff --git a/tools/onboarding-mcp/src/data/docsMap.ts b/tools/onboarding-mcp/src/data/docsMap.ts new file mode 100644 index 0000000000..32bde965c9 --- /dev/null +++ b/tools/onboarding-mcp/src/data/docsMap.ts @@ -0,0 +1,159 @@ +const GITHUB_BASE = "https://github.com/microsoft/vscode-cmake-tools/blob/main"; + +export interface DocsEntry { + keywords: string[]; + file: string; + githubUrl: string; + summary: string; + keyHeadings: string[]; +} + +function doc( + keywords: string[], + file: string, + summary: string, + keyHeadings: string[] +): DocsEntry { + return { + keywords, + file, + githubUrl: `${GITHUB_BASE}/${file}`, + summary, + keyHeadings + }; +} + +export const docsMap: DocsEntry[] = [ + doc( + ["kit", "kits", "compiler", "toolchain"], + "docs/kits.md", + "Documents how kits are found, defined, and configured — including user-local kits, project kits, and scan behavior.", + ["How kits are found and defined", "Kit options", "Specify a compiler", "Specify a toolchain", "Visual Studio"] + ), + doc( + ["variant", "variants", "build type", "debug release", "cmake-variants"], + "docs/variants.md", + "Documents the cmake-variants.yaml schema and how variants apply build configurations like Debug/Release and shared/static linkage.", + ["Example YAML variants file", "Variant schema", "Variant settings", "Variant options", "How variants are applied"] + ), + doc( + ["preset", "presets", "cmakepresets", "cmake presets"], + "docs/cmake-presets.md", + "Full reference for CMakePresets.json support including configure, build, test, package, and workflow presets.", + [ + "Configure and build with CMake Presets", + "Supported CMake and CMakePresets.json versions", + "Enable CMakePresets.json", + "Configure and build", + "Add new presets", + "Edit presets" + ] + ), + doc( + ["settings", "config", "configuration", "cmake settings", "settings.json"], + "docs/cmake-settings.md", + "Reference for all cmake-tools settings in settings.json, including variable substitution and build problem matchers.", + ["CMake settings", "Variable substitution", "Environment variables", "Command substitution", "Additional build problem matchers"] + ), + doc( + ["configure", "cmake configure", "configuration process"], + "docs/configure.md", + "Explains the CMake configure step — how it is triggered, what happens internally, clean configure, and CMake Debugger.", + ["The CMake Tools configure step", "The configure step outside of CMake Tools", "Clean configure", "Configure with CMake Debugger"] + ), + doc( + ["build", "compile", "cmake build", "build target"], + "docs/build.md", + "Explains how to build the default target, a single target, build tasks, build flags, and clean build.", + ["Build the default target", "Build a single target", "Create a build task", "How CMake Tools builds", "Clean build"] + ), + doc( + ["debug", "debugging", "launch", "debug target", "launch.json"], + "docs/debug-launch.md", + "Documents launch targets, quick debugging (no launch.json), launch.json integration for gdb/lldb/msvc, and debugging tests.", + ["Select a launch target", "Debugging without a launch.json", "Debug using a launch.json file", "Debugging tests", "Run without debugging"] + ), + doc( + ["cmake debug", "cmake script debug", "cmake debugger", "cmakelist debug"], + "docs/debug.md", + "Documents debugging CMake scripts themselves (not the built binary) — stepping through CMakeLists.txt.", + ["Debugging from CMake Tools UI entry points", "Debugging from launch.json", "Example launch.json"] + ), + doc( + ["troubleshoot", "troubleshooting", "error", "problem", "common issues"], + "docs/troubleshoot.md", + "Common issues and resolutions, logging levels, log file locations, and how to get help.", + ["Common Issues and Resolutions", "Increase the logging level", "Check the log file", "Get help"] + ), + doc( + ["faq", "frequently asked", "questions", "help"], + "docs/faq.md", + "Frequently asked questions about CMake Tools — getting help, detecting VS Code, learning CMake, and common tasks.", + ["How can I get help?", "How can I detect when CMake is run from VS Code?", "How do I learn about CMake?", "How do I perform common tasks"] + ), + doc( + ["task", "tasks", "tasks.json", "cmake task"], + "docs/tasks.md", + "Documents the VS Code task integration for CMake operations — configure, build, test, install, and clean tasks.", + ["Configure with CMake Tools tasks", "Build with CMake Tools tasks", "Test with CMake Tools tasks", "Install/Clean/Clean-rebuild with CMake Tools tasks"] + ), + doc( + ["how to", "howto", "getting started", "quick start", "tutorial"], + "docs/how-to.md", + "Quick how-to guide for common operations — creating projects, configuring, building, debugging, and IntelliSense setup.", + ["Create a new project", "Configure a project", "Build a project", "Debug a project", "Set up include paths for C++ IntelliSense"] + ), + doc( + ["options", "cmake options", "visibility", "status bar config"], + "docs/cmake-options-configuration.md", + "Documents CMake options visibility configuration — controlling which commands and status bar items are shown.", + ["Default Settings Json", "Configuring your CMake Status Bar and Project Status View"] + ) +]; + +/** Get all known topic keywords for listing in error messages */ +export function knownTopics(): string[] { + const topics = new Set(); + for (const entry of docsMap) { + for (const kw of entry.keywords) { + topics.add(kw); + } + } + return [...topics].sort(); +} + +/** + * Find the best matching docs entry for a topic string. + * Returns entries sorted by keyword match count (best first). + */ +export function findDocsEntries(topic: string): DocsEntry[] { + const inputWords = topic + .toLowerCase() + .split(/[\s,./\\]+/) + .filter((w) => w.length > 1); + + const inputPhrase = topic.toLowerCase().trim(); + + const scored = docsMap + .map((entry) => { + let score = 0; + for (const keyword of entry.keywords) { + if (inputPhrase.includes(keyword.toLowerCase())) { + score += 2; + } else { + const keywordWords = keyword.toLowerCase().split(/\s+/); + for (const kw of keywordWords) { + if (inputWords.includes(kw)) { + score += 1; + } + } + } + } + return { entry, score }; + }) + .filter((e) => e.score > 0) + .sort((a, b) => b.score - a.score) + .map((e) => e.entry); + + return scored; +} diff --git a/tools/onboarding-mcp/src/data/labels.ts b/tools/onboarding-mcp/src/data/labels.ts new file mode 100644 index 0000000000..9f8855a47c --- /dev/null +++ b/tools/onboarding-mcp/src/data/labels.ts @@ -0,0 +1,34 @@ +export interface RepoLabel { + name: string; + description: string; + /** If true, issues with this label are generally not code changes — skip for newcomers. */ + skipForContributors?: boolean; +} + +export const knownLabels: RepoLabel[] = [ + { + name: "bug", + description: "Something is broken" + }, + { + name: "enhancement", + description: "New feature or improvement" + }, + { + name: "documentation", + description: "Documentation gap or error" + }, + { + name: "good first issue", + description: "Explicitly tagged for newcomers — great starting point" + }, + { + name: "help wanted", + description: "Maintainers are looking for community contribution" + }, + { + name: "question", + description: "Not a code change — usually a support request", + skipForContributors: true + } +]; diff --git a/tools/onboarding-mcp/src/data/prRules.ts b/tools/onboarding-mcp/src/data/prRules.ts new file mode 100644 index 0000000000..1ad9c3f4fc --- /dev/null +++ b/tools/onboarding-mcp/src/data/prRules.ts @@ -0,0 +1,46 @@ +export interface PrRule { + id: string; + rule: string; + hint: string; + /** If the user's change description contains any of these keywords, flag the rule as "warn". */ + warnKeywords?: string[]; +} + +export const prRules: PrRule[] = [ + { + id: "changelog", + rule: "Updated CHANGELOG.md as part of the PR", + hint: "Add an entry under the current version in CHANGELOG.md describing your user-visible change. Follow the existing tense and category style (Features / Improvements / Bug Fixes)." + }, + { + id: "naming-convention", + rule: "New variables use lowerCamelCase, not snake_case", + hint: "snake_case was used historically but is being phased out. All new variables must use lowerCamelCase." + }, + { + id: "yarn-lock-unchanged", + rule: "yarn.lock was NOT modified and NOT committed", + hint: "If you deleted .npmrc for local dev, yarn.lock may have changed. Revert it before committing: git checkout yarn.lock" + }, + { + id: "npmrc-unchanged", + rule: ".npmrc was NOT committed", + hint: "The .npmrc file points to an Azure Artifacts feed and must not be modified or deleted in your PR." + }, + { + id: "lint-passes", + rule: "yarn run lint was run and passes", + hint: "Run 'yarn run lint' locally and fix all warnings and errors before submitting your PR." + }, + { + id: "coding-guidelines", + rule: "TypeScript coding guidelines followed", + hint: "Follow the TypeScript coding guidelines: https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines. Code is formatted using the default TypeScript formatter in VS Code with 4-space indentation." + }, + { + id: "dependency-changes", + rule: "If package.json dependencies were added or changed, the team was pinged in the PR", + hint: "Dependency changes require intervention from the CMake Tools team because the repo uses an Azure Artifacts feed. Mention @microsoft/vscode-cmake-tools in the PR.", + warnKeywords: ["package.json", "dependency", "dependencies", "npm install", "yarn add", "added a package", "updated a package", "new dependency"] + } +]; diff --git a/tools/onboarding-mcp/src/data/setupSteps.ts b/tools/onboarding-mcp/src/data/setupSteps.ts new file mode 100644 index 0000000000..c248fe8d58 --- /dev/null +++ b/tools/onboarding-mcp/src/data/setupSteps.ts @@ -0,0 +1,55 @@ +export interface SetupStep { + step: number; + title: string; + command?: string; + notes?: string; +} + +export const setupSteps: SetupStep[] = [ + { + step: 1, + title: "Install Node.js", + notes: "Download and install from https://nodejs.org/en/. The LTS version is recommended." + }, + { + step: 2, + title: "Install yarn globally", + command: "npm install -g yarn", + notes: "yarn is used to compile the code and manage dependencies in this repo." + }, + { + step: 3, + title: "Clone the repo and open it in VS Code", + command: "git clone https://github.com/microsoft/vscode-cmake-tools.git && code vscode-cmake-tools", + notes: "You can also open the cloned folder from VS Code's File > Open Folder menu." + }, + { + step: 4, + title: "Delete .npmrc for local development", + command: "rm .npmrc", + notes: "The repo points the package manager to a public Azure Artifacts feed via .npmrc. Deleting it lets you use the default npm registry for local dev. Do NOT commit this deletion." + }, + { + step: 5, + title: "Install dependencies", + command: "yarn install", + notes: "Installs all required packages. If you skipped step 4, this may fail due to the Azure Artifacts feed requiring authentication." + }, + { + step: 6, + title: "Build and run the extension", + command: "Press F5 in VS Code", + notes: "This compiles the extension and launches it in a new Extension Development Host window. You can set breakpoints in the TypeScript source before pressing F5." + }, + { + step: 7, + title: "Lint the code", + command: "yarn run lint", + notes: "Run this before submitting a PR. Warnings from eslint appear in the Errors and Warnings pane. Install the VS Code eslint extension for real-time feedback." + }, + { + step: 8, + title: "Do NOT commit .npmrc or yarn.lock changes", + notes: "Changes to .npmrc and yarn.lock from using the public npm registry must not be pushed. These files are configured for the project's Azure Artifacts feed." + } +]; diff --git a/tools/onboarding-mcp/src/data/sourceMap.ts b/tools/onboarding-mcp/src/data/sourceMap.ts new file mode 100644 index 0000000000..5d839af4ef --- /dev/null +++ b/tools/onboarding-mcp/src/data/sourceMap.ts @@ -0,0 +1,295 @@ +const GITHUB_BASE = "https://github.com/microsoft/vscode-cmake-tools/blob/main"; + +export interface SourceEntry { + keywords: string[]; + files: Array<{ + path: string; + githubUrl: string; + description: string; + }>; +} + +function entry(keywords: string[], files: Array<{ path: string; description: string }>): SourceEntry { + return { + keywords, + files: files.map((f) => ({ + ...f, + githubUrl: `${GITHUB_BASE}/${f.path}` + })) + }; +} + +export const sourceMap: SourceEntry[] = [ + entry( + ["kit", "kits", "compiler", "toolchain", "scan", "compiler scan", "kit scan"], + [ + { path: "src/kits/kit.ts", description: "Kit type definitions, kit scanning, and compiler detection logic." }, + { path: "src/kits/kitsController.ts", description: "Kit controller — manages the active kit, scanning UI, and kit selection." } + ] + ), + entry( + ["variant", "variants", "debug release", "build type", "cmake-variants"], + [ + { path: "src/kits/variant.ts", description: "Variant (build matrix) schema parsing and application of build configurations." } + ] + ), + entry( + ["preset", "presets", "cmakepresets", "cmakepresets.json", "cmake presets", "user preset"], + [ + { path: "src/presets/preset.ts", description: "CMake Presets type definitions and interfaces for all preset types." }, + { path: "src/presets/presetsController.ts", description: "Presets controller — loading, merging, expanding, and watching preset files." }, + { path: "src/presets/presetsParser.ts", description: "Parser for CMakePresets.json / CMakeUserPresets.json files." } + ] + ), + entry( + ["driver", "cmake process", "cmake communication", "cmake driver"], + [ + { path: "src/drivers/cmakeDriver.ts", description: "Abstract base class for CMake drivers — shared configure/build/target logic." }, + { path: "src/drivers/cmakeFileApiDriver.ts", description: "CMake File API driver — the modern default; reads query/reply files." }, + { path: "src/drivers/cmakeLegacyDriver.ts", description: "Legacy CMake driver for older CMake versions." } + ] + ), + entry( + ["file api", "cmake file api", "query", "reply", "code model"], + [ + { path: "src/drivers/cmakeFileApi.ts", description: "CMake File API query/reply parsing and code model extraction." }, + { path: "src/drivers/cmakeFileApiDriver.ts", description: "File API driver that writes queries and reads replies from .cmake/api/v1/." }, + { path: "src/drivers/codeModel.ts", description: "CodeModelContent types — the authoritative source for targets and file groups." } + ] + ), + entry( + ["build", "compile", "build runner", "cmake build", "build output"], + [ + { path: "src/cmakeBuildRunner.ts", description: "Build-process orchestration, output streaming, and build progress." }, + { path: "src/cmakeProject.ts", description: "Per-folder project state including the full build lifecycle." } + ] + ), + entry( + ["configure", "configuration", "cmake configure", "generate"], + [ + { path: "src/cmakeProject.ts", description: "Per-folder project state — orchestrates the configure step." }, + { path: "src/drivers/cmakeDriver.ts", description: "Abstract driver base — runs cmake with the correct arguments for configure." } + ] + ), + entry( + ["test", "ctest", "test runner", "test explorer", "test results"], + [ + { path: "src/ctest.ts", description: "CTest integration — parses test output, maps tests to VS Code Test Explorer." } + ] + ), + entry( + ["task", "tasks", "vscode task", "task provider", "tasks.json"], + [ + { path: "src/cmakeTaskProvider.ts", description: "VS Code task provider — registers 'cmake' task type for configure/build/test." } + ] + ), + entry( + ["intellisense", "cpptools", "include path", "compile commands", "code completion"], + [ + { path: "src/cpptools.ts", description: "CppTools configuration provider — passes compile flags and includes to C/C++ IntelliSense." }, + { path: "src/compilationDatabase.ts", description: "compile_commands.json handling for external tool consumption." } + ] + ), + entry( + ["debug", "launch", "debugger", "debug target", "launch.json"], + [ + { path: "src/debug/debugger.ts", description: "Debug/launch orchestration — wires CMake targets to VS Code debug sessions." }, + { path: "src/debug/cmakeDebugger/debugConfigurationProvider.ts", description: "Debug configuration provider for CMake script debugging." } + ] + ), + entry( + ["extension", "entry point", "activate", "command registration", "extension.ts"], + [ + { path: "src/extension.ts", description: "Extension activation, command registration, and ExtensionManager singleton." } + ] + ), + entry( + ["project", "workspace", "multi-root", "multi-folder", "project controller"], + [ + { path: "src/projectController.ts", description: "Multi-folder workspace management and active-project routing." }, + { path: "src/workspace.ts", description: "Workspace utility functions." } + ] + ), + entry( + ["setting", "settings", "config", "configuration setting", "cmake setting"], + [ + { path: "src/config.ts", description: "ConfigurationReader — canonical typed access to all extension settings." } + ] + ), + entry( + ["status bar", "status", "sidebar", "ui", "project status"], + [ + { path: "src/status.ts", description: "Status bar items and visibility logic." }, + { path: "src/ui/projectStatus.ts", description: "Project Status sidebar view controller." } + ] + ), + entry( + ["logging", "log", "output channel", "logger"], + [ + { path: "src/logging.ts", description: "Logging infrastructure — createLogger() for module-scoped loggers." } + ] + ), + entry( + ["expand", "variable substitution", "cmake variable", "variable expansion"], + [ + { path: "src/expand.ts", description: "Variable expansion (${variable}) for both kit-context and preset-context vars." } + ] + ), + entry( + ["state", "persistent", "workspace state", "extension state"], + [ + { path: "src/state.ts", description: "Persistent extension state storage across VS Code sessions." } + ] + ), + entry( + ["cpack", "package", "packaging", "installer"], + [ + { path: "src/cpack.ts", description: "CPack integration — runs cpack to create distributable packages." } + ] + ), + entry( + ["coverage", "code coverage", "test coverage"], + [ + { path: "src/coverage.ts", description: "Code coverage support for CTest results." } + ] + ), + entry( + ["diagnostics", "error", "warning", "build error", "compiler output", "problem matcher"], + [ + { path: "src/diagnostics/build.ts", description: "Build diagnostics collection and routing." }, + { path: "src/diagnostics/gcc.ts", description: "GCC/Clang diagnostic output parser." }, + { path: "src/diagnostics/msvc.ts", description: "MSVC diagnostic output parser." }, + { path: "src/diagnostics/cmake.ts", description: "CMake output consumer — parses cmake stdout/stderr." } + ] + ), + entry( + ["outline", "project outline", "target tree", "tree view"], + [ + { path: "src/ui/projectOutline/projectOutline.ts", description: "Project Outline tree view — shows targets and source files." }, + { path: "src/ui/projectOutline/targetsViewCodeModel.ts", description: "Code model to tree-view mapping for the project outline." } + ] + ), + entry( + ["workflow", "workflow preset"], + [ + { path: "src/workflow.ts", description: "Workflow driver — chains configure/build/test/package presets into one sequence." } + ] + ), + entry( + ["telemetry", "telemetry event"], + [ + { path: "src/telemetry.ts", description: "Telemetry helpers — use logEvent() instead of the VS Code telemetry API directly." } + ] + ), + entry( + ["rollbar", "error boundary", "error handling"], + [ + { path: "src/rollbar.ts", description: "Top-level error boundaries — rollbar.invokeAsync() wraps event handlers." } + ] + ) +]; + +/** + * Find source entries whose keywords overlap with words in the input string. + * Returns entries sorted by number of keyword matches (best first). + */ +export function findSourceEntries(feature: string): Array { + const inputWords = feature + .toLowerCase() + .split(/[\s,./\\]+/) + .filter((w) => w.length > 1); + + const inputPhrase = feature.toLowerCase(); + + const scored = sourceMap + .map((entry) => { + let matchCount = 0; + for (const keyword of entry.keywords) { + // Exact phrase match in input (handles multi-word keywords like "file api") + if (inputPhrase.includes(keyword.toLowerCase())) { + matchCount += 2; + } else { + // Single-word keyword overlap + const keywordWords = keyword.toLowerCase().split(/\s+/); + for (const kw of keywordWords) { + if (inputWords.includes(kw)) { + matchCount += 1; + } + } + } + } + return { ...entry, matchCount }; + }) + .filter((e) => e.matchCount > 0) + .sort((a, b) => b.matchCount - a.matchCount); + + return scored; +} + +/** + * Given a text string (e.g. a commit message), return the area names that match. + * An "area" is derived from the first keyword of each SourceEntry. + * Returns deduplicated area names, or ["general"] if nothing matched. + * + * Uses stricter matching than the interactive `findSourceEntries`: + * - Multi-word keywords (e.g. "file api", "cmake driver") match as exact substrings. + * - Single-word keywords only match if they appear as whole words (word-boundary match) + * AND are specific enough (> 4 chars) to avoid false positives from common English words + * like "build", "test", "debug" that appear in most commit messages. + * - File-path-like patterns (e.g. "ctest.ts", "kitsController") are always high-signal. + */ +export function matchAreas(text: string): string[] { + const lower = text.toLowerCase(); + + // Very short common keywords that cause false positives in commit messages. + // These only match when they appear near a file path or as a standalone technical term. + const overlyGeneric = new Set([ + "build", "compile", "test", "debug", "launch", "log", "error", + "warning", "status", "state", "config", "setting", "settings", + "configure", "configuration", "package", "query", "reply", "ui" + ]); + + const matched = new Set(); + for (const entry of sourceMap) { + const areaName = entry.keywords[0]; + let found = false; + + for (const keyword of entry.keywords) { + const kw = keyword.toLowerCase(); + + if (kw.includes(" ")) { + // Multi-word keyword: exact substring match — high confidence + if (lower.includes(kw)) { + found = true; + break; + } + } else if (!overlyGeneric.has(kw)) { + // Specific single-word keyword: word-boundary match + const re = new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`); + if (re.test(lower)) { + found = true; + break; + } + } + // overlyGeneric single-word keywords are skipped unless they're part + // of a multi-word keyword that matched above. + } + + // Also check if any file paths from this entry appear in the text + if (!found) { + for (const file of entry.files) { + const filename = file.path.split("/").pop()?.toLowerCase() ?? ""; + if (filename && lower.includes(filename)) { + found = true; + break; + } + } + } + + if (found) { + matched.add(areaName); + } + } + + return matched.size > 0 ? [...matched] : ["general"]; +} diff --git a/tools/onboarding-mcp/src/github.ts b/tools/onboarding-mcp/src/github.ts new file mode 100644 index 0000000000..337d547554 --- /dev/null +++ b/tools/onboarding-mcp/src/github.ts @@ -0,0 +1,49 @@ +export const REPO = "microsoft/vscode-cmake-tools"; +export const REPO_URL = `https://github.com/${REPO}`; +const API_BASE = `https://api.github.com/repos/${REPO}`; + +export class GitHubApiError extends Error { + constructor( + public readonly status: number, + public readonly statusText: string, + public readonly body: string + ) { + const isRateLimit = status === 403 && body.includes("rate limit"); + const message = isRateLimit + ? `GitHub API rate limit exceeded (HTTP ${status}). ` + + `Unauthenticated requests are limited to 60/hour. ` + + `Set the GITHUB_TOKEN environment variable to increase the limit to 5,000/hour. ` + + `You can create a personal access token at https://github.com/settings/tokens (no scopes needed for public repos).` + : `GitHub API error: HTTP ${status} ${statusText} — ${body}`; + super(message); + this.name = "GitHubApiError"; + } +} + +/** + * Perform a GET request to the GitHub REST API for the cmake-tools repo. + * @param path API path relative to the repo, e.g. "/issues?state=open" + * @returns Parsed JSON response body. + */ +export async function githubGet(path: string): Promise { + const url = `${API_BASE}${path}`; + + const headers: Record = { + "User-Agent": "cmake-tools-onboarding-mcp", + "Accept": "application/vnd.github+json" + }; + + const token = process.env.GITHUB_TOKEN; + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + + const response = await fetch(url, { headers }); + + if (!response.ok) { + const body = await response.text(); + throw new GitHubApiError(response.status, response.statusText, body); + } + + return (await response.json()) as T; +} diff --git a/tools/onboarding-mcp/src/index.ts b/tools/onboarding-mcp/src/index.ts new file mode 100644 index 0000000000..7b4f07702a --- /dev/null +++ b/tools/onboarding-mcp/src/index.ts @@ -0,0 +1,31 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { registerSetupTool } from "./tools/setup.js"; +import { registerPrChecklistTool } from "./tools/prChecklist.js"; +import { registerConceptTool } from "./tools/concepts.js"; +import { registerCodeMapTool } from "./tools/codeMap.js"; +import { registerDocsTool } from "./tools/docs.js"; +import { registerIssuesTool } from "./tools/issues.js"; +import { registerChangelogTool } from "./tools/changelog.js"; + +const server = new McpServer({ + name: "cmake-tools-onboarding", + version: "1.0.0" +}); + +// Phase 1 tools +registerSetupTool(server); +registerPrChecklistTool(server); + +// Phase 2 tools +registerConceptTool(server); +registerCodeMapTool(server); +registerDocsTool(server); + +// Phase 3 tools (live GitHub data) +registerIssuesTool(server); +registerChangelogTool(server); + +// Start the server over stdio +const transport = new StdioServerTransport(); +await server.connect(transport); diff --git a/tools/onboarding-mcp/src/tools/changelog.ts b/tools/onboarding-mcp/src/tools/changelog.ts new file mode 100644 index 0000000000..955b69ddd7 --- /dev/null +++ b/tools/onboarding-mcp/src/tools/changelog.ts @@ -0,0 +1,113 @@ +import { z } from "zod"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { githubGet, GitHubApiError, REPO_URL } from "../github.js"; +import { matchAreas } from "../data/sourceMap.js"; + +interface GitHubCommit { + sha: string; + commit: { + message: string; + author: { + date: string; + }; + }; + author: { + login: string; + } | null; + html_url: string; +} + +export function registerChangelogTool(server: McpServer): void { + server.registerTool( + "get_recent_changes", + { + title: "Get Recent Changes", + description: + "Fetches the most recent commits to the main branch of vscode-cmake-tools. " + + "Each commit is annotated with affected areas (derived from keywords in the commit message " + + "matched against the codebase source map). Includes a summary of the most active areas " + + "to help new contributors understand what's currently in flux.", + inputSchema: z.object({ + limit: z + .number() + .int() + .min(1) + .max(30) + .default(10) + .describe("Number of recent commits to fetch (default 10, max 30).") + }) + }, + async ({ limit }) => { + const effectiveLimit = Math.min(limit ?? 10, 30); + + try { + const raw = await githubGet( + `/commits?per_page=${effectiveLimit}` + ); + + // Track area frequencies for the summary + const areaFrequency = new Map(); + + const commits = raw.map((c) => { + const firstLine = c.commit.message.split("\n")[0].trim(); + const areas = matchAreas(c.commit.message); + + for (const area of areas) { + areaFrequency.set(area, (areaFrequency.get(area) ?? 0) + 1); + } + + return { + sha: c.sha.slice(0, 7), + message: firstLine, + author: c.author?.login ?? "unknown", + date: c.commit.author.date, + url: c.html_url, + affectedAreas: areas + }; + }); + + // Top 3 most active areas + const mostActiveAreas = [...areaFrequency.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, 3) + .map(([area]) => area); + + const areaList = mostActiveAreas.join(", ") || "general"; + const tip = + mostActiveAreas.length > 0 && !mostActiveAreas.includes("general") + ? `Recent activity is concentrated in ${areaList} — if you're new, these areas may have more in-flux context to catch up on.` + : "Recent commits span a broad range of areas. Check the affectedAreas on each commit to find patterns."; + + const result = { + commits, + summary: { + fetchedAt: new Date().toISOString(), + repoUrl: REPO_URL, + totalReturned: commits.length, + mostActiveAreas, + tip + } + }; + + return { + content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] + }; + } catch (error) { + const message = + error instanceof GitHubApiError + ? error.message + : `Failed to fetch commits: ${error instanceof Error ? error.message : String(error)}`; + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ error: message }, null, 2) + } + ], + isError: true + }; + } + } + ); +} diff --git a/tools/onboarding-mcp/src/tools/codeMap.ts b/tools/onboarding-mcp/src/tools/codeMap.ts new file mode 100644 index 0000000000..32a7cd7500 --- /dev/null +++ b/tools/onboarding-mcp/src/tools/codeMap.ts @@ -0,0 +1,90 @@ +import { z } from "zod"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { findSourceEntries, sourceMap } from "../data/sourceMap.js"; + +export function registerCodeMapTool(server: McpServer): void { + server.registerTool( + "find_source_file", + { + title: "Find Source File", + description: + "Given a natural-language description of a feature or area of the codebase " + + "(e.g. 'kit scanning', 'build logic', 'test runner', 'launch config'), " + + "returns matching source files with GitHub links, descriptions, and relevance notes. " + + "If no match is found, returns suggestions of searchable topics.", + inputSchema: z.object({ + feature: z + .string() + .describe( + "A natural-language description of the feature or area — " + + "e.g. 'where is kit scanning', 'build logic', 'test runner', 'launch config', 'status bar'." + ) + }) + }, + async ({ feature }) => { + const results = findSourceEntries(feature); + + if (results.length === 0) { + // Collect all unique keywords for suggestions + const allKeywords = new Set(); + for (const entry of sourceMap) { + for (const kw of entry.keywords) { + allKeywords.add(kw); + } + } + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify( + { + matches: [], + tip: `No matches found for "${feature}". Try searching for one of these topics: ${[...allKeywords].sort().join(", ")}.` + }, + null, + 2 + ) + } + ] + }; + } + + // Flatten matched entries into individual file results + const matches = results.flatMap((entry) => + entry.files.map((f) => ({ + file: f.path, + githubUrl: f.githubUrl, + description: f.description, + relevance: `Matched keywords: ${entry.keywords.filter((kw) => feature.toLowerCase().includes(kw.toLowerCase()) || kw.toLowerCase().split(/\s+/).some((w) => feature.toLowerCase().includes(w))).join(", ")}` + })) + ); + + // Deduplicate by file path, keeping the first (highest-relevance) occurrence + const seen = new Set(); + const deduplicated = matches.filter((m) => { + if (seen.has(m.file)) { + return false; + } + seen.add(m.file); + return true; + }); + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify( + { + matches: deduplicated, + tip: "Start with the first match — it has the highest relevance to your query. Use 'explain_concept' for a deeper overview of any concept area." + }, + null, + 2 + ) + } + ] + }; + } + ); +} diff --git a/tools/onboarding-mcp/src/tools/concepts.ts b/tools/onboarding-mcp/src/tools/concepts.ts new file mode 100644 index 0000000000..d4370ee501 --- /dev/null +++ b/tools/onboarding-mcp/src/tools/concepts.ts @@ -0,0 +1,69 @@ +import { z } from "zod"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { findConcept, knownConceptNames } from "../data/concepts.js"; + +export function registerConceptTool(server: McpServer): void { + server.registerTool( + "explain_concept", + { + title: "Explain Concept", + description: + "Explains a CMake Tools extension concept (e.g. 'kit', 'preset', 'driver', 'ctest'). " + + "Returns a summary, detailed explanation, related concepts, relevant source files, " + + "and a link to the documentation page. " + + "If the concept is not recognized, returns a list of all known concepts.", + inputSchema: z.object({ + concept: z + .string() + .describe( + "The concept to explain — e.g. 'kit', 'preset', 'variant', 'driver', 'ctest', " + + "'configure', 'build', 'task', 'intellisense', 'cpptools', 'debug', 'extension', 'settings'." + ) + }) + }, + async ({ concept }) => { + const entry = findConcept(concept); + + if (!entry) { + const known = knownConceptNames(); + return { + content: [ + { + type: "text" as const, + text: JSON.stringify( + { + error: `Unknown concept: "${concept}"`, + knownConcepts: known, + hint: "Try one of the known concepts listed above, or use a common alias (e.g. 'test' for 'ctest', 'config' for 'settings')." + }, + null, + 2 + ) + } + ] + }; + } + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify( + { + concept: entry.concept, + summary: entry.summary, + details: entry.details, + relatedConcepts: entry.relatedConcepts, + sourceFiles: entry.sourceFiles, + docsPage: entry.docsPage, + docsUrl: entry.docsUrl + }, + null, + 2 + ) + } + ] + }; + } + ); +} diff --git a/tools/onboarding-mcp/src/tools/docs.ts b/tools/onboarding-mcp/src/tools/docs.ts new file mode 100644 index 0000000000..63fb58855a --- /dev/null +++ b/tools/onboarding-mcp/src/tools/docs.ts @@ -0,0 +1,76 @@ +import { z } from "zod"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { findDocsEntries, knownTopics } from "../data/docsMap.js"; + +export function registerDocsTool(server: McpServer): void { + server.registerTool( + "get_docs_page", + { + title: "Get Docs Page", + description: + "Given a topic (e.g. 'presets', 'kits', 'debugging', 'settings', 'troubleshooting'), " + + "returns the matching documentation page with its file path, GitHub URL, a summary, and key headings. " + + "If no match is found, returns a list of all known topics.", + inputSchema: z.object({ + topic: z + .string() + .describe( + "The documentation topic — e.g. 'presets', 'kits', 'debugging', 'settings', " + + "'troubleshooting', 'faq', 'build', 'configure', 'tasks', 'variants'." + ) + }) + }, + async ({ topic }) => { + const entries = findDocsEntries(topic); + + if (entries.length === 0) { + const topics = knownTopics(); + return { + content: [ + { + type: "text" as const, + text: JSON.stringify( + { + error: `No documentation found for topic: "${topic}"`, + knownTopics: topics, + hint: "Try one of the known topics listed above." + }, + null, + 2 + ) + } + ] + }; + } + + // Return the best match as the primary result, with additional matches as related pages + const best = entries[0]; + const related = entries.slice(1).map((e) => ({ + file: e.file, + githubUrl: e.githubUrl, + summary: e.summary + })); + + const result: Record = { + topic, + file: best.file, + githubUrl: best.githubUrl, + summary: best.summary, + keyHeadings: best.keyHeadings + }; + + if (related.length > 0) { + result.relatedPages = related; + } + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(result, null, 2) + } + ] + }; + } + ); +} diff --git a/tools/onboarding-mcp/src/tools/issues.ts b/tools/onboarding-mcp/src/tools/issues.ts new file mode 100644 index 0000000000..5107a833a4 --- /dev/null +++ b/tools/onboarding-mcp/src/tools/issues.ts @@ -0,0 +1,128 @@ +import { z } from "zod"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { githubGet, GitHubApiError, REPO_URL } from "../github.js"; +import { knownLabels } from "../data/labels.js"; + +interface GitHubIssue { + number: number; + title: string; + html_url: string; + labels: Array<{ name: string }>; + comments: number; + created_at: string; + updated_at: string; + body: string | null; + pull_request?: unknown; +} + +function truncateBody(body: string | null, maxLen = 300): string { + if (!body) { + return ""; + } + const trimmed = body.trim(); + if (trimmed.length <= maxLen) { + return trimmed; + } + return trimmed.slice(0, maxLen).trimEnd() + "..."; +} + +function isRecentlyUpdated(updatedAt: string, daysThreshold = 90): boolean { + const updated = new Date(updatedAt).getTime(); + const cutoff = Date.now() - daysThreshold * 24 * 60 * 60 * 1000; + return updated >= cutoff; +} + +export function registerIssuesTool(server: McpServer): void { + server.registerTool( + "get_contributor_issues", + { + title: "Get Contributor Issues", + description: + "Fetches recently updated open issues from the vscode-cmake-tools GitHub repo " + + "and enriches each with contributor-friendliness signals (good-first-issue label, " + + "recency, comment count, etc.) so Copilot can reason about what's workable for " + + "a new contributor. Optionally filter by label (e.g. 'bug', 'enhancement', 'good first issue'). " + + "Known labels: " + knownLabels.map((l) => `"${l.name}"`).join(", ") + ".", + inputSchema: z.object({ + limit: z + .number() + .int() + .min(1) + .max(50) + .default(20) + .describe("Number of issues to fetch (default 20, max 50)."), + label: z + .string() + .optional() + .describe( + "Optional label filter, e.g. 'bug', 'enhancement', 'good first issue'. " + + "If omitted, returns all recently updated open issues." + ) + }) + }, + async ({ limit, label }) => { + const effectiveLimit = Math.min(limit ?? 20, 50); + + let path = `/issues?state=open&sort=updated&direction=desc&per_page=${effectiveLimit}`; + if (label) { + path += `&labels=${encodeURIComponent(label)}`; + } + + try { + const raw = await githubGet(path); + + // Filter out pull requests (GitHub's issues endpoint includes PRs) + const issues = raw.filter((i) => !i.pull_request); + + const enriched = issues.map((issue) => { + const labelNames = issue.labels.map((l) => l.name); + return { + number: issue.number, + title: issue.title, + url: issue.html_url, + labels: labelNames, + commentCount: issue.comments, + createdAt: issue.created_at, + updatedAt: issue.updated_at, + body: truncateBody(issue.body), + contributorSignals: { + hasGoodFirstIssueLabel: labelNames.includes("good first issue"), + isRecentlyUpdated: isRecentlyUpdated(issue.updated_at), + hasLowCommentCount: issue.comments < 5, + hasNoBugLabel: !labelNames.includes("bug") + } + }; + }); + + const result = { + issues: enriched, + meta: { + fetchedAt: new Date().toISOString(), + repoUrl: REPO_URL, + totalReturned: enriched.length, + tip: "Issues with hasGoodFirstIssueLabel or low comment counts are often the best starting point for new contributors. Feature and documentation issues (hasNoBugLabel) tend to be safer than bug fixes for newcomers." + } + }; + + return { + content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] + }; + } catch (error) { + const message = + error instanceof GitHubApiError + ? error.message + : `Failed to fetch issues: ${error instanceof Error ? error.message : String(error)}`; + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ error: message }, null, 2) + } + ], + isError: true + }; + } + } + ); +} diff --git a/tools/onboarding-mcp/src/tools/prChecklist.ts b/tools/onboarding-mcp/src/tools/prChecklist.ts new file mode 100644 index 0000000000..5700719745 --- /dev/null +++ b/tools/onboarding-mcp/src/tools/prChecklist.ts @@ -0,0 +1,60 @@ +import { z } from "zod"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { prRules } from "../data/prRules.js"; + +export interface ChecklistItem { + rule: string; + status: "pass" | "warn" | "manual_check_required"; + hint: string; +} + +export function registerPrChecklistTool(server: McpServer): void { + server.registerTool( + "check_pr_readiness", + { + title: "Check PR Readiness", + description: + "Given a free-text description of what a contributor changed, returns a checklist " + + "of PR requirements derived from CONTRIBUTING.md. Each item has a rule, a status " + + '("pass", "warn", or "manual_check_required"), and a hint. Since we cannot ' + + "inspect the actual diff, most items are 'manual_check_required' with a helpful " + + "hint. Items are flagged as 'warn' when the description suggests a potential issue.", + inputSchema: z.object({ + changes: z + .string() + .describe("A free-text description of what the contributor changed.") + }) + }, + async ({ changes }) => { + const lowerChanges = changes.toLowerCase(); + + const checklist: ChecklistItem[] = prRules.map((rule) => { + let status: ChecklistItem["status"] = "manual_check_required"; + + // Flag as "warn" if the change description contains any warn keywords + if ( + rule.warnKeywords?.some((keyword) => + lowerChanges.includes(keyword.toLowerCase()) + ) + ) { + status = "warn"; + } + + return { + rule: rule.rule, + status, + hint: rule.hint + }; + }); + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(checklist, null, 2) + } + ] + }; + } + ); +} diff --git a/tools/onboarding-mcp/src/tools/setup.ts b/tools/onboarding-mcp/src/tools/setup.ts new file mode 100644 index 0000000000..bef3c6ac6b --- /dev/null +++ b/tools/onboarding-mcp/src/tools/setup.ts @@ -0,0 +1,23 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { setupSteps } from "../data/setupSteps.js"; + +export function registerSetupTool(server: McpServer): void { + server.registerTool( + "get_setup_guide", + { + title: "Get Setup Guide", + description: + "Returns an ordered list of setup steps a new contributor needs to follow " + + "to build and run the CMake Tools VS Code extension locally. " + + "Each step includes a number, title, optional command, and optional notes." + }, + async () => ({ + content: [ + { + type: "text" as const, + text: JSON.stringify(setupSteps, null, 2) + } + ] + }) + ); +} diff --git a/tools/onboarding-mcp/tsconfig.json b/tools/onboarding-mcp/tsconfig.json new file mode 100644 index 0000000000..0c1345af08 --- /dev/null +++ b/tools/onboarding-mcp/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +}