diff --git a/packages/mcp/.gitignore b/packages/mcp/.gitignore
deleted file mode 100644
index 23bfe49c8..000000000
--- a/packages/mcp/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-dist/
-node_modules/
\ No newline at end of file
diff --git a/packages/mcp/.npmignore b/packages/mcp/.npmignore
deleted file mode 100644
index f09a7667c..000000000
--- a/packages/mcp/.npmignore
+++ /dev/null
@@ -1,5 +0,0 @@
-**/*
-!/dist/**
-!README.md
-!package.json
-!CHANGELOG.md
\ No newline at end of file
diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md
deleted file mode 100644
index be0de1097..000000000
--- a/packages/mcp/CHANGELOG.md
+++ /dev/null
@@ -1,117 +0,0 @@
-# Changelog
-
-All notable changes to this project will be documented in this file.
-
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
-and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-
-## [Unreleased]
-
-## [1.0.18] - 2026-02-28
-
-### Changed
-- Bumped `@modelcontextprotocol/sdk` from 1.10.2 to 1.26.0. [#958](https://github.com/sourcebot-dev/sourcebot/pull/958)
-- Bumped `zod` from `^3.24.3` to `^4.3.6`. [#964](https://github.com/sourcebot-dev/sourcebot/pull/964)
-
-## [1.0.17] - 2026-02-19
-
-### Added
-- Added optional `visibility` parameter to `ask_codebase` tool to allow controlling chat session visibility in shared environments. [#903](https://github.com/sourcebot-dev/sourcebot/pull/903)
-- Added `defaultBranch`, `isFork`, and `isArchived` fields to the `list_repos` tool response. [#905](https://github.com/sourcebot-dev/sourcebot/pull/905)
-
-### Changed
-- Changed `SOURCEBOT_HOST` to default to `http://localhost:3000` instead of `https://demo.sourcebot.dev`, which is now deprecated. [#906](https://github.com/sourcebot-dev/sourcebot/pull/906)
-
-## [1.0.16] - 2026-02-10
-
-### Added
-- Added `list_tree` tool for listing files/directories in a repository path with depth controls, suitable for both directory listings and repo-tree workflows. [#870](https://github.com/sourcebot-dev/sourcebot/pull/870)
-
-## [1.0.15] - 2026-02-02
-
-### Added
-- Added `ask_codebase` tool that can invoke the Ask subagent to explore a set of codebases and return a summarized answer. [#814](https://github.com/sourcebot-dev/sourcebot/pull/814)
-- Added `list_language_models` tool to discover available language models configured on the Sourcebot instance. [#814](https://github.com/sourcebot-dev/sourcebot/pull/814)
-
-## [1.0.14] - 2026-01-27
-
-### Changed
-- Updated README.
-
-## [1.0.13] - 2026-01-27
-
-### Added
-- Added `search_commits` tool to search a repos commit history. [#625](https://github.com/sourcebot-dev/sourcebot/pull/625)
-- Added `gitRevision` parameter to the `search_code` tool to allow for searching on different branches. [#625](https://github.com/sourcebot-dev/sourcebot/pull/625)
-- Added server side pagination support for `list_commits` and `list_repos`. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
-- Added `filterByFilepaths` and `useRegex` params to the `search_code` tool. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
-
-### Changed
-- Renamed `search_commits` tool to `list_commits`. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
-- Renamed `gitRevision` param to `ref` on `search_code` tool. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
-- Generally improved tool and tool param descriptions for all tools. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
-
-## [1.0.12] - 2026-01-13
-
-### Fixed
-- Fixed invalid file and url MCP results for local indexed repos [#718](https://github.com/sourcebot-dev/sourcebot/pull/718)
-
-## [1.0.11] - 2025-12-03
-
-### Changed
-- Updated API client to match the latest Sourcebot release. [#652](https://github.com/sourcebot-dev/sourcebot/pull/652)
-
-## [1.0.10] - 2025-11-24
-
-### Changed
-- Updated API client to match the latest Sourcebot release. [#555](https://github.com/sourcebot-dev/sourcebot/pull/555)
-
-## [1.0.9] - 2025-11-17
-
-### Added
-- Added pagination and filtering to `list_repos` tool to handle large repository lists efficiently and prevent oversized responses that waste token context. [#614](https://github.com/sourcebot-dev/sourcebot/pull/614)
-
-## [1.0.8] - 2025-11-10
-
-### Fixed
-- Fixed issue where search results exceeding token limits would be completely discarded instead of returning truncated content. [#604](https://github.com/sourcebot-dev/sourcebot/pull/604)
-
-## [1.0.7] - 2025-10-28
-
-### Changed
-- Updated API client to match the latest Sourcebot release. [#555](https://github.com/sourcebot-dev/sourcebot/pull/555)
-
-## [1.0.6] - 2025-09-26
-
-### Fixed
-- Fix `linkedConnections is required` schema error.
-
-## [1.0.5] - 2025-09-15
-
-### Changed
-- Updated API client to match the latest Sourcebot release. [#356](https://github.com/sourcebot-dev/sourcebot/pull/356)
-
-## [1.0.4] - 2025-08-04
-
-### Fixed
-- Fixed issue where console logs were resulting in "unexpected token" errors on the MCP client. [#429](https://github.com/sourcebot-dev/sourcebot/pull/429)
-
-## [1.0.3] - 2025-06-18
-
-### Changed
-- Updated API client to match the latest Sourcebot release. [#356](https://github.com/sourcebot-dev/sourcebot/pull/356)
-
-## [1.0.2] - 2025-05-28
-
-### Changed
-- Added API key support. [#311](https://github.com/sourcebot-dev/sourcebot/pull/311)
-
-## [1.0.1] - 2025-05-15
-
-### Changed
-- Updated API client to match the latest Sourcebot release. [#307](https://github.com/sourcebot-dev/sourcebot/pull/307)
-
-## [1.0.0] - 2025-05-07
-
-### Added
-- Initial release
diff --git a/packages/mcp/Dockerfile b/packages/mcp/Dockerfile
deleted file mode 100644
index 8e72653de..000000000
--- a/packages/mcp/Dockerfile
+++ /dev/null
@@ -1,27 +0,0 @@
-# Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config
-# syntax=docker/dockerfile:1
-
-# Builder stage
-FROM node:lts-alpine AS builder
-WORKDIR /app
-
-# Install dependencies and build
-COPY package.json tsconfig.json ./
-COPY src ./src
-RUN npm install
-RUN npm run build
-
-# Final stage
-FROM node:lts-alpine
-WORKDIR /app
-
-# Install only production dependencies
-COPY package.json ./
-RUN npm install --production
-
-# Copy built artifacts
-COPY --from=builder /app/dist ./dist
-
-# Expose no specific port since this is stdio MCP server
-# Default command
-CMD ["node", "dist/index.js"]
diff --git a/packages/mcp/README.md b/packages/mcp/README.md
deleted file mode 100644
index c8431c203..000000000
--- a/packages/mcp/README.md
+++ /dev/null
@@ -1,299 +0,0 @@
-# Sourcebot MCP - Fetch code context from GitHub, GitLab, Bitbucket, and more
-
-[](https://sourcebot.dev)
-[](https://github.com/sourcebot-dev/sourcebot)
-[](https://docs.sourcebot.dev/docs/features/mcp-server)
-[](https://www.npmjs.com/package/@sourcebot/mcp)
-
-The Sourcebot MCP server gives your LLM agents the ability to fetch code context across thousands of repos hosted on [GitHub](https://docs.sourcebot.dev/docs/connections/github), [GitLab](https://docs.sourcebot.dev/docs/connections/gitlab), [BitBucket](https://docs.sourcebot.dev/docs/connections/bitbucket-cloud) and [more](#supported-code-hosts). Ask your LLM a question, and the Sourcebot MCP server will fetch relevant context from its index and inject it into your chat session. Some use cases this unlocks include:
-
-- Enriching responses to user requests:
- - _"What repositories are using internal library X?"_
- - _"Provide usage examples of the CodeMirror component"_
- - _"Where is the `useCodeMirrorTheme` hook defined?"_
- - _"Find all usages of `deprecatedApi` across all repos"_
-
-- Improving reasoning ability for existing horizontal agents like AI code review, docs generation, etc.
- - _"Find the definitions for all functions in this diff"_
- - _"Document what systems depend on this class"_
-
-- Building custom LLM horizontal agents like like compliance auditing agents, migration agents, etc.
- - _"Find all instances of hardcoded credentials"_
- - _"Identify repositories that depend on this deprecated api"_
-
-
-## Getting Started
-
-1. Install Node.JS >= v18.0.0.
-
-2. (optional) Spin up a Sourcebot instance by following [this guide](https://docs.sourcebot.dev/self-hosting/overview). The host url of your instance (e.g., `http://localhost:3000`) is passed to the MCP server via the `SOURCEBOT_HOST` url. This allows you to control which repos Sourcebot MCP fetches context from (including private repos).
-
- If a host is not provided, then the server will fallback to using the demo instance hosted at https://app.sourcebot.dev. You can see the list of repositories indexed [here](https://app.sourcebot.dev/~/repos). Add additional repositories by [opening a PR](https://github.com/sourcebot-dev/sourcebot/blob/main/demo-site-config.json).
-
-3. Install `@sourcebot/mcp` into your MCP client:
-
-
- Cursor
-
- [Cursor MCP docs](https://docs.cursor.com/context/model-context-protocol)
-
- Go to: `Settings` -> `Cursor Settings` -> `MCP` -> `Add new global MCP server`
-
- Paste the following into your `~/.cursor/mcp.json` file. This will install Sourcebot globally within Cursor:
-
- ```json
- {
- "mcpServers": {
- "sourcebot": {
- "command": "npx",
- "args": ["-y", "@sourcebot/mcp@latest" ],
- // Optional - if not specified, https://app.sourcebot.dev is used
- "env": {
- "SOURCEBOT_HOST": "http://localhost:3000"
- }
- }
- }
- }
- ```
-
-
-
- Windsurf
-
- [Windsurf MCP docs](https://docs.windsurf.com/windsurf/mcp)
-
- Go to: `Windsurf Settings` -> `Cascade` -> `Add Server` -> `Add Custom Server`
-
- Paste the following into your `mcp_config.json` file:
-
- ```json
- {
- "mcpServers": {
- "sourcebot": {
- "command": "npx",
- "args": ["-y", "@sourcebot/mcp@latest" ],
- // Optional - if not specified, https://app.sourcebot.dev is used
- "env": {
- "SOURCEBOT_HOST": "http://localhost:3000"
- }
- }
- }
- }
- ```
-
-
-
- VS Code
-
- [VS Code MCP docs](https://code.visualstudio.com/docs/copilot/chat/mcp-servers)
-
- Add the following to your [.vscode/mcp.json](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server-to-your-workspace) file:
-
- ```json
- {
- "servers": {
- "sourcebot": {
- "type": "stdio",
- "command": "npx",
- "args": ["-y", "@sourcebot/mcp@latest"],
- // Optional - if not specified, https://app.sourcebot.dev is used
- "env": {
- "SOURCEBOT_HOST": "http://localhost:3000"
- }
- }
- }
- }
- ```
-
-
-
-
- Claude Code
-
- [Claude Code MCP docs](https://docs.anthropic.com/en/docs/claude-code/tutorials#set-up-model-context-protocol-mcp)
-
- Run the following command:
-
- ```sh
- # SOURCEBOT_HOST env var is optional - if not specified,
- # https://app.sourcebot.dev is used.
- claude mcp add sourcebot -e SOURCEBOT_HOST=http://localhost:3000 -- npx -y @sourcebot/mcp@latest
- ```
-
-
-
- Claude Desktop
-
- [Claude Desktop MCP docs](https://modelcontextprotocol.io/quickstart/user)
-
- Add the following to your `claude_desktop_config.json`:
-
- ```json
- {
- "mcpServers": {
- "sourcebot": {
- "command": "npx",
- "args": ["-y", "@sourcebot/mcp@latest"],
- // Optional - if not specified, https://app.sourcebot.dev is used
- "env": {
- "SOURCEBOT_HOST": "http://localhost:3000"
- }
- }
- }
- }
- ```
-
-
-
- Alternatively, you can install using via [Smithery](https://smithery.ai/server/@sourcebot-dev/sourcebot). For example:
-
- ```bash
- npx -y @smithery/cli install @sourcebot-dev/sourcebot --client claude
- ```
-
-
-
-4. Tell your LLM to `use sourcebot` when prompting.
-
-
-
-For a more detailed guide, checkout [the docs](https://docs.sourcebot.dev/docs/features/mcp-server).
-
-
-## Available Tools
-
-### search_code
-
-Searches for code that matches the provided search query as a substring by default, or as a regular expression if `useRegex` is true.
-
-
-Parameters
-
-| Name | Required | Description |
-|:----------------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------|
-| `query` | yes | The search pattern to match against code contents. Do not escape quotes in your query. |
-| `useRegex` | no | Whether to use regular expression matching. When false, substring matching is used (default: false). |
-| `filterByRepos` | no | Scope the search to specific repositories. |
-| `filterByLanguages` | no | Scope the search to specific languages. |
-| `filterByFilepaths` | no | Scope the search to specific filepaths. |
-| `caseSensitive` | no | Whether the search should be case sensitive (default: false). |
-| `includeCodeSnippets` | no | Whether to include code snippets in the response (default: false). |
-| `ref` | no | Commit SHA, branch or tag name to search on. If not provided, defaults to the default branch. |
-| `maxTokens` | no | The maximum number of tokens to return (default: 10000). Higher values provide more context but consume more tokens. |
-
-
-
-### list_repos
-
-Lists repositories indexed by Sourcebot with optional filtering and pagination.
-
-
-Parameters
-
-| Name | Required | Description |
-|:------------|:---------|:--------------------------------------------------------------------------------|
-| `query` | no | Filter repositories by name (case-insensitive). |
-| `page` | no | Page number for pagination (min 1, default: 1). |
-| `perPage` | no | Results per page for pagination (min 1, max 100, default: 30). |
-| `sort` | no | Sort repositories by 'name' or 'pushed' (most recent commit). Default: 'name'. |
-| `direction` | no | Sort direction: 'asc' or 'desc' (default: 'asc'). |
-
-
-
-### read_file
-
-Reads the source code for a given file.
-
-
-Parameters
-
-| Name | Required | Description |
-|:-------|:---------|:---------------------------------------------------------------------------------------------------------------|
-| `repo` | yes | The repository name. |
-| `path` | yes | The path to the file. |
-| `ref` | no | Commit SHA, branch or tag name to fetch the source code for. If not provided, uses the default branch. |
-
-
-### list_tree
-
-Lists files and directories from a repository path. Can be used as a directory listing tool (`depth: 1`) or a repo-tree tool (`depth > 1`).
-
-
-Parameters
-
-| Name | Required | Description |
-|:---------------------|:---------|:--------------------------------------------------------------------------------------------------------------|
-| `repo` | yes | The name of the repository to list files from. |
-| `path` | no | Directory path (relative to repo root). If omitted, the repo root is used. |
-| `ref` | no | Commit SHA, branch or tag name to list files from. If not provided, uses the default branch. |
-| `depth` | no | Number of directory levels to traverse below `path` (min 1, max 10, default: 1). |
-| `includeFiles` | no | Whether to include file entries in the output (default: true). |
-| `includeDirectories` | no | Whether to include directory entries in the output (default: true). |
-| `maxEntries` | no | Maximum number of entries to return before truncating (min 1, max 10000, default: 1000). |
-
-
-
-### list_commits
-
-Get a list of commits for a given repository.
-
-
-Parameters
-
-| Name | Required | Description |
-|:----------|:---------|:--------------------------------------------------------------------------------------------------------------------------------------|
-| `repo` | yes | The name of the repository to list commits for. |
-| `query` | no | Search query to filter commits by message content (case-insensitive). |
-| `since` | no | Show commits more recent than this date. Supports ISO 8601 (e.g., '2024-01-01') or relative formats (e.g., '30 days ago'). |
-| `until` | no | Show commits older than this date. Supports ISO 8601 (e.g., '2024-12-31') or relative formats (e.g., 'yesterday'). |
-| `author` | no | Filter commits by author name or email (case-insensitive). |
-| `ref` | no | Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch. |
-| `page` | no | Page number for pagination (min 1, default: 1). |
-| `perPage` | no | Results per page for pagination (min 1, max 100, default: 50). |
-
-
-
-### list_language_models
-
-Lists the available language models configured on the Sourcebot instance. Use this to discover which models can be specified when calling `ask_codebase`.
-
-
-Parameters
-
-This tool takes no parameters.
-
-
-
-### ask_codebase
-
-Ask a natural language question about the codebase. This tool uses an AI agent to autonomously search code, read files, and find symbol references/definitions to answer your question. Returns a detailed answer in markdown format with code references, plus a link to view the full research session in the Sourcebot web UI.
-
-
-Parameters
-
-| Name | Required | Description |
-|:----------------|:---------|:-----------------------------------------------------------------------------------------------------------------------------------------------|
-| `query` | yes | The query to ask about the codebase. |
-| `repos` | no | The repositories that are accessible to the agent during the chat. If not provided, all repositories are accessible. |
-| `languageModel` | no | The language model to use for answering the question. Object with `provider` and `model`. If not provided, defaults to the first model in the config. Use `list_language_models` to see available options. |
-| `visibility` | no | The visibility of the chat session (`'PRIVATE'` or `'PUBLIC'`). Defaults to `PRIVATE` for authenticated users and `PUBLIC` for anonymous users. Set to `PUBLIC` to make the chat viewable by anyone with the link (useful in shared environments like Slack). |
-
-
-
-
-## Supported Code Hosts
-Sourcebot supports the following code hosts:
-- [GitHub](https://docs.sourcebot.dev/docs/connections/github)
-- [GitLab](https://docs.sourcebot.dev/docs/connections/gitlab)
-- [Bitbucket Cloud](https://docs.sourcebot.dev/docs/connections/bitbucket-cloud)
-- [Bitbucket Data Center](https://docs.sourcebot.dev/docs/connections/bitbucket-data-center)
-- [Gitea](https://docs.sourcebot.dev/docs/connections/gitea)
-- [Gerrit](https://docs.sourcebot.dev/docs/connections/gerrit)
-
-| Don't see your code host? Open a [feature request](https://github.com/sourcebot-dev/sourcebot/issues/new?template=feature_request.md).
-
-## Future Work
-
-### Semantic Search
-
-Currently, Sourcebot only supports regex-based code search (powered by [zoekt](https://github.com/sourcegraph/zoekt) under the hood). It is great for scenarios when the agent is searching for is something that is super precise and well-represented in the source code (e.g., a specific function name, a error string, etc.). It is not-so-great for _fuzzy_ searches where the objective is to find some loosely defined _category_ or _concept_ in the code (e.g., find code that verifies JWT tokens). The LLM can approximate this by crafting regex searches that attempt to capture a concept (e.g., it might try a query like `"jwt|token|(verify|validate).*(jwt|token)"`), but often yields sub-optimal search results that aren't related. Tools like Cursor solve this with [embedding models](https://docs.cursor.com/context/codebase-indexing) to capture the semantic meaning of code, allowing for LLMs to search using natural language. We would like to extend Sourcebot to support semantic search and expose this capability over MCP as a tool (e.g., `semantic_search_code` tool). [GitHub Discussion](https://github.com/sourcebot-dev/sourcebot/discussions/297)
diff --git a/packages/mcp/package.json b/packages/mcp/package.json
deleted file mode 100644
index 074179b18..000000000
--- a/packages/mcp/package.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "name": "@sourcebot/mcp",
- "version": "1.0.18",
- "type": "module",
- "main": "dist/index.js",
- "types": "dist/index.d.ts",
- "scripts": {
- "build": "tsc",
- "dev": "node ./dist/index.js",
- "build:watch": "tsc-watch --preserveWatchOutput"
- },
- "devDependencies": {
- "@types/express": "^5.0.1",
- "@types/node": "^20.0.0",
- "tsc-watch": "6.2.1",
- "tsx": "^4.21.0",
- "typescript": "^5.0.0"
- },
- "dependencies": {
- "@modelcontextprotocol/sdk": "^1.26.0",
- "@t3-oss/env-core": "^0.13.4",
- "dedent": "^1.7.1",
- "escape-string-regexp": "^5.0.0",
- "express": "^5.1.0",
- "zod": "^4.3.6"
- },
- "bin": {
- "sourcebot-mcp": "./dist/index.js"
- },
- "repository": {
- "type": "git",
- "url": "https://github.com/sourcebot-dev/sourcebot.git",
- "directory": "packages/mcp"
- },
- "publishConfig": {
- "access": "public",
- "registry": "https://registry.npmjs.org/"
- },
- "keywords": [
- "mcp",
- "modelcontextprotocol",
- "code-search",
- "sourcebot",
- "code-intelligence"
- ]
-}
diff --git a/packages/mcp/smithery.yaml b/packages/mcp/smithery.yaml
deleted file mode 100644
index 06974cba5..000000000
--- a/packages/mcp/smithery.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-# Smithery configuration file: https://smithery.ai/docs/build/project-config
-
-startCommand:
- type: stdio
- configSchema:
- # JSON Schema defining the configuration options for the MCP.
- type: object
- required: []
- properties:
- sourcebotHost:
- type: string
- description: Optional URL of the Sourcebot server (e.g., http://localhost:3000).
- commandFunction:
- # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
- |-
- (config) => {
- const env = {};
- if (config.sourcebotHost) {
- env.SOURCEBOT_HOST = config.sourcebotHost;
- }
- return { command: 'node', args: ['dist/index.js'], env };
- }
- exampleConfig:
- sourcebotHost: http://localhost:3000
diff --git a/packages/mcp/src/client.ts b/packages/mcp/src/client.ts
deleted file mode 100644
index 52613e2f7..000000000
--- a/packages/mcp/src/client.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-import { env } from './env.js';
-import { listReposResponseSchema, searchResponseSchema, fileSourceResponseSchema, listCommitsResponseSchema, askCodebaseResponseSchema, listLanguageModelsResponseSchema, listTreeApiResponseSchema } from './schemas.js';
-import { AskCodebaseRequest, AskCodebaseResponse, FileSourceRequest, ListReposQueryParams, SearchRequest, ListCommitsQueryParamsSchema, ListLanguageModelsResponse, ListTreeApiRequest, ListTreeApiResponse } from './types.js';
-import { isServiceError, ServiceErrorException } from './utils.js';
-import { z } from 'zod';
-
-const parseResponse = async (
- response: Response,
- schema: T
-): Promise> => {
- const text = await response.text();
-
- let json: unknown;
- try {
- json = JSON.parse(text);
- } catch {
- throw new Error(`Invalid JSON response: ${text}`);
- }
-
- // Check if the response is already a service error from the API
- if (isServiceError(json)) {
- throw new ServiceErrorException(json);
- }
-
- const parsed = schema.safeParse(json);
- if (!parsed.success) {
- throw new Error(`Failed to parse response: ${parsed.error.message}`);
- }
-
- return parsed.data;
-};
-
-export const search = async (request: SearchRequest) => {
- const response = await fetch(`${env.SOURCEBOT_HOST}/api/search`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Sourcebot-Client-Source': 'mcp',
- ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
- },
- body: JSON.stringify(request)
- });
-
- return parseResponse(response, searchResponseSchema);
-}
-
-export const listRepos = async (queryParams: ListReposQueryParams = {}) => {
- const url = new URL(`${env.SOURCEBOT_HOST}/api/repos`);
-
- for (const [key, value] of Object.entries(queryParams)) {
- if (value) {
- url.searchParams.set(key, value.toString());
- }
- }
-
- const response = await fetch(url, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Sourcebot-Client-Source': 'mcp',
- ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
- },
- });
-
- const repos = await parseResponse(response, listReposResponseSchema);
- const totalCount = parseInt(response.headers.get('X-Total-Count') ?? '0', 10);
- return { repos, totalCount };
-}
-
-export const getFileSource = async (request: FileSourceRequest) => {
- const url = new URL(`${env.SOURCEBOT_HOST}/api/source`);
- for (const [key, value] of Object.entries(request)) {
- if (value) {
- url.searchParams.set(key, value.toString());
- }
- }
-
- const response = await fetch(url, {
- method: 'GET',
- headers: {
- 'X-Sourcebot-Client-Source': 'mcp',
- ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
- },
- });
-
- return parseResponse(response, fileSourceResponseSchema);
-}
-
-export const listCommits = async (queryParams: ListCommitsQueryParamsSchema) => {
- const url = new URL(`${env.SOURCEBOT_HOST}/api/commits`);
- for (const [key, value] of Object.entries(queryParams)) {
- if (value) {
- url.searchParams.set(key, value.toString());
- }
- }
-
- const response = await fetch(url, {
- method: 'GET',
- headers: {
- 'X-Org-Domain': '~',
- 'X-Sourcebot-Client-Source': 'mcp',
- ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
- },
- });
-
- const commits = await parseResponse(response, listCommitsResponseSchema);
- const totalCount = parseInt(response.headers.get('X-Total-Count') ?? '0', 10);
- return { commits, totalCount };
-}
-
-/**
- * Fetches a repository tree (or subtree union) from the Sourcebot tree API.
- *
- * @param request - Repository name, revision, and path selectors for the tree query
- * @returns A tree response rooted at `tree` containing nested `tree`/`blob` nodes
- */
-export const listTree = async (request: ListTreeApiRequest): Promise => {
- const response = await fetch(`${env.SOURCEBOT_HOST}/api/tree`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Sourcebot-Client-Source': 'mcp',
- ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
- },
- body: JSON.stringify(request),
- });
-
- return parseResponse(response, listTreeApiResponseSchema);
-}
-
-/**
- * Asks a natural language question about the codebase using the Sourcebot AI agent.
- * This is a blocking call that runs the full agent loop and returns when complete.
- *
- * @param request - The question and optional repo filters
- * @returns The agent's answer, chat URL, sources, and metadata
- */
-export const askCodebase = async (request: AskCodebaseRequest): Promise => {
- const response = await fetch(`${env.SOURCEBOT_HOST}/api/chat/blocking`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Sourcebot-Client-Source': 'mcp',
- ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
- },
- body: JSON.stringify(request),
- });
-
- return parseResponse(response, askCodebaseResponseSchema);
-}
-
-/**
- * Lists the available language models configured on the Sourcebot instance.
- *
- * @returns Array of language model info objects
- */
-export const listLanguageModels = async (): Promise => {
- const response = await fetch(`${env.SOURCEBOT_HOST}/api/models`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Sourcebot-Client-Source': 'mcp',
- ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
- },
- });
-
- return parseResponse(response, listLanguageModelsResponseSchema);
-}
diff --git a/packages/mcp/src/env.ts b/packages/mcp/src/env.ts
deleted file mode 100644
index 28d014f45..000000000
--- a/packages/mcp/src/env.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { createEnv } from "@t3-oss/env-core";
-import { z } from "zod";
-
-export const numberSchema = z.coerce.number();
-
-const SOURCEBOT_DEFAULT_HOST = "http://localhost:3000";
-
-export const env = createEnv({
- server: {
- SOURCEBOT_HOST: z.string().url().default(SOURCEBOT_DEFAULT_HOST),
-
- SOURCEBOT_API_KEY: z.string().optional(),
-
- // The minimum number of tokens to return
- DEFAULT_MINIMUM_TOKENS: numberSchema.default(10000),
-
- // The number of matches to fetch from the search API.
- DEFAULT_MATCHES: numberSchema.default(10000),
-
- // The number of lines to include above and below a match
- DEFAULT_CONTEXT_LINES: numberSchema.default(5),
- },
- runtimeEnv: process.env,
- emptyStringAsUndefined: true,
-});
\ No newline at end of file
diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts
deleted file mode 100644
index 866739966..000000000
--- a/packages/mcp/src/index.ts
+++ /dev/null
@@ -1,463 +0,0 @@
-#!/usr/bin/env node
-
-// Entry point for the MCP server
-import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
-import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
-import _dedent from "dedent";
-import escapeStringRegexp from 'escape-string-regexp';
-import { z } from 'zod';
-import { askCodebase, getFileSource, listCommits, listLanguageModels, listRepos, listTree, search } from './client.js';
-import { env, numberSchema } from './env.js';
-import { askCodebaseRequestSchema, DEFAULT_MAX_TREE_ENTRIES, DEFAULT_TREE_DEPTH, fileSourceRequestSchema, listCommitsQueryParamsSchema, listReposQueryParamsSchema, listTreeRequestSchema, MAX_MAX_TREE_ENTRIES, MAX_TREE_DEPTH } from './schemas.js';
-import { AskCodebaseRequest, FileSourceRequest, ListCommitsQueryParamsSchema, ListReposQueryParams, ListTreeEntry, ListTreeRequest, TextContent } from './types.js';
-import { buildTreeNodeIndex, joinTreePath, normalizeTreePath, sortTreeEntries } from './utils.js';
-
-const dedent = _dedent.withOptions({ alignValues: true });
-
-// Create MCP server
-const server = new McpServer({
- name: 'sourcebot-mcp-server',
- version: '0.1.0',
-});
-
-
-server.tool(
- "search_code",
- dedent`
- Searches for code that matches the provided search query as a substring by default, or as a regular expression if useRegex is true. Useful for exploring remote repositories by searching for exact symbols, functions, variables, or specific code patterns. To determine if a repository is indexed, use the \`list_repos\` tool. By default, searches are global and will search the default branch of all repositories. Searches can be scoped to specific repositories, languages, and branches. When referencing code outputted by this tool, always include the file's external URL as a link. This makes it easier for the user to view the file, even if they don't have it locally checked out.
- `,
- {
- query: z
- .string()
- .describe(`The search pattern to match against code contents. Do not escape quotes in your query.`)
- // Escape backslashes first, then quotes, and wrap in double quotes
- // so the query is treated as a literal phrase (like grep).
- .transform((val) => {
- const escaped = val.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
- return `"${escaped}"`;
- }),
- useRegex: z
- .boolean()
- .describe(`Whether to use regular expression matching to match the search query against code contents. When false, substring matching is used. (default: false)`)
- .optional(),
- filterByRepos: z
- .array(z.string())
- .describe(`Scope the search to the provided repositories.`)
- .optional(),
- filterByLanguages: z
- .array(z.string())
- .describe(`Scope the search to the provided languages.`)
- .optional(),
- filterByFilepaths: z
- .array(z.string())
- .describe(`Scope the search to the provided filepaths.`)
- .optional(),
- caseSensitive: z
- .boolean()
- .describe(`Whether the search should be case sensitive (default: false).`)
- .optional(),
- includeCodeSnippets: z
- .boolean()
- .describe(`Whether to include the code snippets in the response. If false, only the file's URL, repository, and language will be returned. (default: false)`)
- .optional(),
- ref: z
- .string()
- .describe(`Commit SHA, branch or tag name to search on. If not provided, defaults to the default branch (usually 'main' or 'master').`)
- .optional(),
- maxTokens: numberSchema
- .describe(`The maximum number of tokens to return (default: ${env.DEFAULT_MINIMUM_TOKENS}). Higher values provide more context but consume more tokens. Values less than ${env.DEFAULT_MINIMUM_TOKENS} will be ignored.`)
- .transform((val) => (val < env.DEFAULT_MINIMUM_TOKENS ? env.DEFAULT_MINIMUM_TOKENS : val))
- .optional(),
- },
- { readOnlyHint: true },
- async ({
- query,
- filterByRepos: repos = [],
- filterByLanguages: languages = [],
- filterByFilepaths: filepaths = [],
- maxTokens = env.DEFAULT_MINIMUM_TOKENS,
- includeCodeSnippets = false,
- caseSensitive = false,
- ref,
- useRegex = false,
- }) => {
- if (repos.length > 0) {
- query += ` (repo:${repos.map(id => escapeStringRegexp(id)).join(' or repo:')})`;
- }
-
- if (languages.length > 0) {
- query += ` (lang:${languages.join(' or lang:')})`;
- }
-
- if (filepaths.length > 0) {
- query += ` (file:${filepaths.map(filepath => escapeStringRegexp(filepath)).join(' or file:')})`;
- }
-
- if (ref) {
- query += ` ( rev:${ref} )`;
- }
-
- const response = await search({
- query,
- matches: env.DEFAULT_MATCHES,
- contextLines: env.DEFAULT_CONTEXT_LINES,
- isRegexEnabled: useRegex,
- isCaseSensitivityEnabled: caseSensitive,
- });
-
- if (response.files.length === 0) {
- return {
- content: [{
- type: "text",
- text: `No results found for the query: ${query}`,
- }],
- };
- }
-
- const content: TextContent[] = [];
- let totalTokens = 0;
- let isResponseTruncated = false;
-
- for (const file of response.files) {
- const numMatches = file.chunks.reduce(
- (acc, chunk) => acc + chunk.matchRanges.length,
- 0,
- );
- let text = dedent`
- file: ${file.webUrl}
- num_matches: ${numMatches}
- repo: ${file.repository}
- language: ${file.language}
- `;
-
- if (includeCodeSnippets) {
- const snippets = file.chunks.map(chunk => {
- return `\`\`\`\n${chunk.content}\n\`\`\``
- }).join('\n');
- text += `\n\n${snippets}`;
- }
-
-
- // Rough estimate of the number of tokens in the text
- // @see: https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them
- const tokens = text.length / 4;
-
- if ((totalTokens + tokens) > maxTokens) {
- // Calculate remaining token budget
- const remainingTokens = maxTokens - totalTokens;
-
- if (remainingTokens > 100) { // Only truncate if meaningful space left
- // Truncate text to fit remaining tokens (tokens ≈ chars/4)
- const maxLength = Math.floor(remainingTokens * 4);
- const truncatedText = text.substring(0, maxLength) + "\n\n...[content truncated due to token limit]";
-
- content.push({
- type: "text",
- text: truncatedText,
- });
-
- totalTokens += remainingTokens;
- }
-
- isResponseTruncated = true;
- break;
- }
-
- totalTokens += tokens;
- content.push({
- type: "text",
- text,
- });
- }
-
- if (isResponseTruncated) {
- content.push({
- type: "text",
- text: `The response was truncated because the number of tokens exceeded the maximum limit of ${maxTokens}.`,
- });
- }
-
- return {
- content,
- }
- }
-);
-
-server.tool(
- "list_commits",
- dedent`Get a list of commits for a given repository.`,
- listCommitsQueryParamsSchema.shape,
- { readOnlyHint: true },
- async (request: ListCommitsQueryParamsSchema) => {
- const result = await listCommits(request);
-
- return {
- content: [{
- type: "text", text: JSON.stringify(result)
- }],
- };
- }
-);
-
-server.tool(
- "list_repos",
- dedent`Lists repositories in the organization with optional filtering and pagination.`,
- listReposQueryParamsSchema.shape,
- { readOnlyHint: true },
- async (request: ListReposQueryParams) => {
- const result = await listRepos(request);
-
- return {
- content: [{
- type: "text", text: JSON.stringify({
- repos: result.repos.map((repo) => ({
- name: repo.repoName,
- url: repo.webUrl,
- pushedAt: repo.pushedAt,
- defaultBranch: repo.defaultBranch,
- isFork: repo.isFork,
- isArchived: repo.isArchived,
- })),
- totalCount: result.totalCount,
- })
- }]
- };
- }
-);
-
-server.tool(
- "read_file",
- dedent`Reads the source code for a given file.`,
- fileSourceRequestSchema.shape,
- { readOnlyHint: true },
- async (request: FileSourceRequest) => {
- const response = await getFileSource(request);
-
- return {
- content: [{
- type: "text", text: JSON.stringify({
- source: response.source,
- language: response.language,
- path: response.path,
- url: response.webUrl,
- })
- }]
- };
- }
-);
-
-server.tool(
- "list_tree",
- dedent`
- Lists files and directories from a repository path. This can be used as a repo tree tool or directory listing tool.
- Returns a flat list of entries with path metadata and depth relative to the requested path.
- `,
- listTreeRequestSchema.shape,
- { readOnlyHint: true },
- async ({
- repo,
- path = '',
- ref = 'HEAD',
- depth = DEFAULT_TREE_DEPTH,
- includeFiles = true,
- includeDirectories = true,
- maxEntries = DEFAULT_MAX_TREE_ENTRIES,
- }: ListTreeRequest) => {
- const normalizedPath = normalizeTreePath(path);
- const normalizedDepth = Math.min(depth, MAX_TREE_DEPTH);
- const normalizedMaxEntries = Math.min(maxEntries, MAX_MAX_TREE_ENTRIES);
-
- if (!includeFiles && !includeDirectories) {
- return {
- content: [{
- type: "text",
- text: JSON.stringify({
- repo,
- ref,
- path: normalizedPath,
- entries: [] as ListTreeEntry[],
- totalReturned: 0,
- truncated: false,
- }),
- }],
- };
- }
-
- // BFS frontier of directories still to expand. Each item stores a repo-relative
- // directory path plus the current depth from the requested root `path`.
- const queue: Array<{ path: string; depth: number }> = [{ path: normalizedPath, depth: 0 }];
-
- // Tracks directory paths that have already been enqueued.
- // With the current single-root traversal duplicates are uncommon, but this
- // prevents duplicate expansion if we later support overlapping multi-root
- // inputs (e.g. ["src", "src/lib"]) or receive overlapping tree data.
- const queuedPaths = new Set([normalizedPath]);
-
- const seenEntries = new Set();
- const entries: ListTreeEntry[] = [];
- let truncated = false;
-
- // Traverse breadth-first by depth, batching all directories at the same
- // depth into a single /api/tree request per iteration.
- while (queue.length > 0 && !truncated) {
- const currentDepth = queue[0]!.depth;
- const currentLevelPaths: string[] = [];
-
- // Drain only the current depth level so we can issue one API call
- // for all sibling directories before moving deeper.
- while (queue.length > 0 && queue[0]!.depth === currentDepth) {
- const next = queue.shift()!;
- currentLevelPaths.push(next.path);
- }
-
- // Ask Sourcebot for a tree spanning all requested paths at this level.
- const treeResponse = await listTree({
- repoName: repo,
- revisionName: ref,
- paths: currentLevelPaths.filter(Boolean),
- });
- const treeNodeIndex = buildTreeNodeIndex(treeResponse.tree);
-
- for (const currentPath of currentLevelPaths) {
- const currentNode = currentPath === '' ? treeResponse.tree : treeNodeIndex.get(currentPath);
- if (!currentNode || currentNode.type !== 'tree') {
- // Skip paths that are missing from the response or resolve to a
- // file node. We only iterate children of directories.
- continue;
- }
-
- for (const child of currentNode.children) {
- if (child.type !== 'tree' && child.type !== 'blob') {
- // Skip non-standard git object types (e.g. unexpected entries)
- // since this tool only exposes directories and files.
- continue;
- }
-
- const childPath = joinTreePath(currentPath, child.name);
- const childDepth = currentDepth + 1;
-
- // Queue child directories for the next depth level only if
- // they are within the requested depth bound.
- if (child.type === 'tree' && childDepth < normalizedDepth && !queuedPaths.has(childPath)) {
- queue.push({ path: childPath, depth: childDepth });
- queuedPaths.add(childPath);
- }
-
- if ((child.type === 'blob' && !includeFiles) || (child.type === 'tree' && !includeDirectories)) {
- // Skip entries filtered out by caller preferences
- // (`includeFiles` / `includeDirectories`).
- continue;
- }
-
- const key = `${child.type}:${childPath}`;
- if (seenEntries.has(key)) {
- // Skip duplicates when multiple requested paths overlap and
- // surface the same child entry.
- continue;
- }
- seenEntries.add(key);
-
- // Stop collecting once the entry budget is exhausted.
- if (entries.length >= normalizedMaxEntries) {
- truncated = true;
- break;
- }
-
- entries.push({
- type: child.type,
- path: childPath,
- name: child.name,
- parentPath: currentPath,
- depth: childDepth,
- });
- }
-
- if (truncated) {
- break;
- }
- }
- }
-
- const sortedEntries = sortTreeEntries(entries);
-
- return {
- content: [{
- type: "text",
- text: JSON.stringify({
- repo,
- ref,
- path: normalizedPath,
- entries: sortedEntries,
- totalReturned: sortedEntries.length,
- truncated,
- }),
- }]
- };
- }
-);
-
-server.tool(
- "list_language_models",
- dedent`Lists the available language models configured on the Sourcebot instance. Use this to discover which models can be specified when calling ask_codebase.`,
- {},
- { readOnlyHint: true },
- async () => {
- const models = await listLanguageModels();
-
- return {
- content: [{
- type: "text",
- text: JSON.stringify(models),
- }],
- };
- }
-);
-
-server.tool(
- "ask_codebase",
- dedent`
- Ask a natural language question about the codebase. This tool uses an AI agent to autonomously search code, read files, and find symbol references/definitions to answer your question.
-
- The agent will:
- - Analyze your question and determine what context it needs
- - Search the codebase using multiple strategies (code search, symbol lookup, file reading)
- - Synthesize findings into a comprehensive answer with code references
-
- Returns a detailed answer in markdown format with code references, plus a link to view the full research session (including all tool calls and reasoning) in the Sourcebot web UI.
-
- When using this in shared environments (e.g., Slack), you can set the visibility parameter to 'PUBLIC' to ensure everyone can access the chat link.
-
- This is a blocking operation that may take 30-60+ seconds for complex questions as the agent researches the codebase.
- `,
- askCodebaseRequestSchema.shape,
- { readOnlyHint: true },
- async (request: AskCodebaseRequest) => {
- const response = await askCodebase(request);
-
- // Format the response with the answer and a link to the chat
- const formattedResponse = dedent`
- ${response.answer}
-
- ---
- **View full research session:** ${response.chatUrl}
- **Model used:** ${response.languageModel.model}
- `;
-
- return {
- content: [{
- type: "text",
- text: formattedResponse,
- }],
- };
- }
-);
-
-const runServer = async () => {
- const transport = new StdioServerTransport();
- await server.connect(transport);
-}
-
-runServer().catch((error) => {
- console.error('Failed to start MCP server:', error);
- process.exit(1);
-});
diff --git a/packages/mcp/src/schemas.ts b/packages/mcp/src/schemas.ts
deleted file mode 100644
index a72fbf116..000000000
--- a/packages/mcp/src/schemas.ts
+++ /dev/null
@@ -1,411 +0,0 @@
-// @NOTE : Please keep this file in sync with @sourcebot/web/src/features/search/types.ts
-// At some point, we should move these to a shared package...
-import { z } from "zod";
-
-export const locationSchema = z.object({
- // 0-based byte offset from the beginning of the file
- byteOffset: z.number(),
- // 1-based line number from the beginning of the file
- lineNumber: z.number(),
- // 1-based column number (in runes) from the beginning of line
- column: z.number(),
-});
-
-export const rangeSchema = z.object({
- start: locationSchema,
- end: locationSchema,
-});
-
-export const symbolSchema = z.object({
- symbol: z.string(),
- kind: z.string(),
-});
-
-export const searchOptionsSchema = z.object({
- matches: z.number(), // The number of matches to return.
- contextLines: z.number().optional(), // The number of context lines to return.
- whole: z.boolean().optional(), // Whether to return the whole file as part of the response.
- isRegexEnabled: z.boolean().optional(), // Whether to enable regular expression search.
- isCaseSensitivityEnabled: z.boolean().optional(), // Whether to enable case sensitivity.
-});
-
-export const searchRequestSchema = z.object({
- query: z.string(), // The zoekt query to execute.
- ...searchOptionsSchema.shape,
-});
-
-export const repositoryInfoSchema = z.object({
- id: z.number(),
- codeHostType: z.string(),
- name: z.string(),
- displayName: z.string().optional(),
- webUrl: z.string().optional(),
-});
-
-// Many of these fields are defined in zoekt/api.go.
-export const searchStatsSchema = z.object({
- // The actual number of matches returned by the search.
- // This will always be less than or equal to `totalMatchCount`.
- actualMatchCount: z.number(),
-
- // The total number of matches found during the search.
- totalMatchCount: z.number(),
-
- // The duration (in nanoseconds) of the search.
- duration: z.number(),
-
- // Number of files containing a match.
- fileCount: z.number(),
-
- // Candidate files whose contents weren't examined because we
- // gathered enough matches.
- filesSkipped: z.number(),
-
- // Amount of I/O for reading contents.
- contentBytesLoaded: z.number(),
-
- // Amount of I/O for reading from index.
- indexBytesLoaded: z.number(),
-
- // Number of search shards that had a crash.
- crashes: z.number(),
-
- // Number of files in shards that we considered.
- shardFilesConsidered: z.number(),
-
- // Files that we evaluated. Equivalent to files for which all
- // atom matches (including negations) evaluated to true.
- filesConsidered: z.number(),
-
- // Files for which we loaded file content to verify substring matches
- filesLoaded: z.number(),
-
- // Shards that we scanned to find matches.
- shardsScanned: z.number(),
-
- // Shards that we did not process because a query was canceled.
- shardsSkipped: z.number(),
-
- // Shards that we did not process because the query was rejected by the
- // ngram filter indicating it had no matches.
- shardsSkippedFilter: z.number(),
-
- // Number of candidate matches as a result of searching ngrams.
- ngramMatches: z.number(),
-
- // NgramLookups is the number of times we accessed an ngram in the index.
- ngramLookups: z.number(),
-
- // Wall clock time for queued search.
- wait: z.number(),
-
- // Aggregate wall clock time spent constructing and pruning the match tree.
- // This accounts for time such as lookups in the trigram index.
- matchTreeConstruction: z.number(),
-
- // Aggregate wall clock time spent searching the match tree. This accounts
- // for the bulk of search work done looking for matches.
- matchTreeSearch: z.number(),
-
- // Number of times regexp was called on files that we evaluated.
- regexpsConsidered: z.number(),
-
- // FlushReason explains why results were flushed.
- flushReason: z.string(),
-});
-
-export const searchResponseSchema = z.object({
- stats: searchStatsSchema,
- files: z.array(z.object({
- fileName: z.object({
- // The name of the file
- text: z.string(),
- // Any matching ranges
- matchRanges: z.array(rangeSchema),
- }),
- webUrl: z.string(),
- externalWebUrl: z.string().optional(),
- repository: z.string(),
- repositoryId: z.number(),
- language: z.string(),
- chunks: z.array(z.object({
- content: z.string(),
- matchRanges: z.array(rangeSchema),
- contentStart: locationSchema,
- symbols: z.array(z.object({
- ...symbolSchema.shape,
- parent: symbolSchema.optional(),
- })).optional(),
- })),
- branches: z.array(z.string()).optional(),
- // Set if `whole` is true.
- content: z.string().optional(),
- })),
- repositoryInfo: z.array(repositoryInfoSchema),
- isSearchExhaustive: z.boolean(),
-});
-
-export const repositoryQuerySchema = z.object({
- codeHostType: z.string(),
- repoId: z.number(),
- repoName: z.string(),
- repoDisplayName: z.string().optional(),
- webUrl: z.string(),
- externalWebUrl: z.string().optional(),
- imageUrl: z.string().optional(),
- indexedAt: z.coerce.date().optional(),
- pushedAt: z.coerce.date().optional(),
- defaultBranch: z.string().optional(),
- isFork: z.boolean(),
- isArchived: z.boolean(),
-});
-
-export const listReposResponseSchema = repositoryQuerySchema.array();
-
-export const listReposQueryParamsSchema = z.object({
- query: z
- .string()
- .describe("Filter repositories by name (case-insensitive)")
- .optional(),
- page: z
- .number()
- .int()
- .positive()
- .describe("Page number for pagination (min 1). Default: 1")
- .optional()
- .default(1),
- perPage: z
- .number()
- .int()
- .positive()
- .max(100)
- .describe("Results per page for pagination (min 1, max 100). Default: 30")
- .optional()
- .default(30),
- sort: z
- .enum(['name', 'pushed'])
- .describe("Sort repositories by 'name' or 'pushed' (most recent commit). Default: 'name'")
- .optional()
- .default('name'),
- direction: z
- .enum(['asc', 'desc'])
- .describe("Sort direction: 'asc' or 'desc'. Default: 'asc'")
- .optional()
- .default('asc'),
-});
-
-export const fileSourceRequestSchema = z.object({
- repo: z
- .string()
- .describe("The repository name."),
- path: z
- .string()
- .describe("The path to the file."),
- ref: z
- .string()
- .optional()
- .describe("Commit SHA, branch or tag name to fetch the source code for. If not provided, uses the default branch of the repository."),
-});
-
-export const fileSourceResponseSchema = z.object({
- source: z.string(),
- language: z.string(),
- path: z.string(),
- repo: z.string(),
- repoCodeHostType: z.string(),
- repoDisplayName: z.string().optional(),
- repoExternalWebUrl: z.string().optional(),
- webUrl: z.string(),
- externalWebUrl: z.string().optional(),
-});
-
-type TreeNode = {
- type: string;
- path: string;
- name: string;
- children: TreeNode[];
-};
-
-const treeNodeSchema: z.ZodType = z.lazy(() => z.object({
- type: z.string(),
- path: z.string(),
- name: z.string(),
- children: z.array(treeNodeSchema),
-}));
-
-export const listTreeApiRequestSchema = z.object({
- repoName: z.string(),
- revisionName: z.string(),
- paths: z.array(z.string()),
-});
-
-export const listTreeApiResponseSchema = z.object({
- tree: treeNodeSchema,
-});
-
-export const DEFAULT_TREE_DEPTH = 1;
-export const MAX_TREE_DEPTH = 10;
-export const DEFAULT_MAX_TREE_ENTRIES = 1000;
-export const MAX_MAX_TREE_ENTRIES = 10000;
-
-export const listTreeRequestSchema = z.object({
- repo: z
- .string()
- .describe("The name of the repository to list files from."),
- path: z
- .string()
- .describe("Directory path (relative to repo root). If omitted, the repo root is used.")
- .optional()
- .default(''),
- ref: z
- .string()
- .describe("Commit SHA, branch or tag name to list files from. If not provided, uses the default branch.")
- .optional()
- .default('HEAD'),
- depth: z
- .number()
- .int()
- .positive()
- .max(MAX_TREE_DEPTH)
- .describe(`How many directory levels to traverse below \`path\` (min 1, max ${MAX_TREE_DEPTH}, default ${DEFAULT_TREE_DEPTH}).`)
- .optional()
- .default(DEFAULT_TREE_DEPTH),
- includeFiles: z
- .boolean()
- .describe("Whether to include files in the output (default: true).")
- .optional()
- .default(true),
- includeDirectories: z
- .boolean()
- .describe("Whether to include directories in the output (default: true).")
- .optional()
- .default(true),
- maxEntries: z
- .number()
- .int()
- .positive()
- .max(MAX_MAX_TREE_ENTRIES)
- .describe(`Maximum number of entries to return (min 1, max ${MAX_MAX_TREE_ENTRIES}, default ${DEFAULT_MAX_TREE_ENTRIES}).`)
- .optional()
- .default(DEFAULT_MAX_TREE_ENTRIES),
-});
-
-export const listTreeEntrySchema = z.object({
- type: z.enum(['tree', 'blob']),
- path: z.string(),
- name: z.string(),
- parentPath: z.string(),
- depth: z.number().int().positive(),
-});
-
-export const listTreeResponseSchema = z.object({
- repo: z.string(),
- ref: z.string(),
- path: z.string(),
- entries: z.array(listTreeEntrySchema),
- totalReturned: z.number().int().nonnegative(),
- truncated: z.boolean(),
-});
-
-export const serviceErrorSchema = z.object({
- statusCode: z.number(),
- errorCode: z.string(),
- message: z.string(),
-});
-
-export const listCommitsQueryParamsSchema = z.object({
- repo: z
- .string()
- .describe("The name of the repository to list commits for."),
- query: z
- .string()
- .describe("Search query to filter commits by message content (case-insensitive).")
- .optional(),
- since: z
- .string()
- .describe(`Show commits more recent than this date. Filters by actual commit time. Supports ISO 8601 (e.g., '2024-01-01') or relative formats (e.g., '30 days ago', 'last week').`)
- .optional(),
- until: z
- .string()
- .describe(`Show commits older than this date. Filters by actual commit time. Supports ISO 8601 (e.g., '2024-12-31') or relative formats (e.g., 'yesterday').`)
- .optional(),
- author: z
- .string()
- .describe(`Filter commits by author name or email (case-insensitive).`)
- .optional(),
- ref: z
- .string()
- .describe("Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch of the repository.")
- .optional(),
- page: z
- .number()
- .int()
- .positive()
- .describe("Page number for pagination (min 1). Default: 1")
- .optional()
- .default(1),
- perPage: z
- .number()
- .int()
- .positive()
- .max(100)
- .describe("Results per page for pagination (min 1, max 100). Default: 50")
- .optional()
- .default(50),
-});
-
-export const listCommitsResponseSchema = z.array(z.object({
- hash: z.string(),
- date: z.string(),
- message: z.string(),
- refs: z.string(),
- body: z.string(),
- author_name: z.string(),
- author_email: z.string(),
-}));
-
-export const languageModelInfoSchema = z.object({
- provider: z.string().describe("The model provider (e.g., 'anthropic', 'openai')"),
- model: z.string().describe("The model ID"),
- displayName: z.string().optional().describe("Optional display name for the model"),
-});
-
-export const listLanguageModelsResponseSchema = z.array(languageModelInfoSchema);
-
-export const askCodebaseRequestSchema = z.object({
- query: z
- .string()
- .describe("The query to ask about the codebase."),
- repos: z
- .array(z.string())
- .optional()
- .describe("The repositories that are accessible to the agent during the chat. If not provided, all repositories are accessible."),
- languageModel: languageModelInfoSchema
- .omit({ displayName: true })
- .optional()
- .describe("The language model to use for answering the question. If not provided, defaults to the first model in the config. Use list_language_models to see available options."),
- visibility: z
- .enum(['PRIVATE', 'PUBLIC'])
- .optional()
- .describe("The visibility of the chat session. If not provided, defaults to PRIVATE for authenticated users and PUBLIC for anonymous users. Set to PUBLIC to make the chat viewable by anyone with the link."),
-});
-
-export const sourceSchema = z.object({
- type: z.literal('file'),
- repo: z.string(),
- path: z.string(),
- name: z.string(),
- language: z.string(),
- revision: z.string(),
-});
-
-export const askCodebaseResponseSchema = z.object({
- answer: z.string().describe("The agent's final answer in markdown format"),
- chatId: z.string().describe("ID of the persisted chat session"),
- chatUrl: z.string().describe("URL to view the chat in the web UI"),
- languageModel: z.object({
- provider: z.string().describe("The model provider (e.g., 'anthropic', 'openai')"),
- model: z.string().describe("The model ID"),
- displayName: z.string().optional().describe("Optional display name for the model"),
- }).describe("The language model used to generate the response"),
-});
diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts
deleted file mode 100644
index 63c050856..000000000
--- a/packages/mcp/src/types.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-// @NOTE : Please keep this file in sync with @sourcebot/web/src/features/search/types.ts
-// At some point, we should move these to a shared package...
-import {
- fileSourceResponseSchema,
- listReposQueryParamsSchema,
- locationSchema,
- searchRequestSchema,
- searchResponseSchema,
- rangeSchema,
- fileSourceRequestSchema,
- symbolSchema,
- serviceErrorSchema,
- listCommitsQueryParamsSchema,
- listCommitsResponseSchema,
- askCodebaseRequestSchema,
- askCodebaseResponseSchema,
- languageModelInfoSchema,
- listLanguageModelsResponseSchema,
- listTreeApiRequestSchema,
- listTreeApiResponseSchema,
- listTreeRequestSchema,
- listTreeEntrySchema,
- listTreeResponseSchema,
-} from "./schemas.js";
-import { z } from "zod";
-
-export type SearchRequest = z.infer;
-export type SearchResponse = z.infer;
-export type SearchResultRange = z.infer;
-export type SearchResultLocation = z.infer;
-export type SearchResultFile = SearchResponse["files"][number];
-export type SearchResultChunk = SearchResultFile["chunks"][number];
-export type SearchSymbol = z.infer;
-
-export type ListReposQueryParams = z.input;
-
-export type FileSourceRequest = z.infer;
-export type FileSourceResponse = z.infer;
-
-export type TextContent = { type: "text", text: string };
-
-export type ServiceError = z.infer;
-
-export type ListCommitsQueryParamsSchema = z.infer;
-export type ListCommitsResponse = z.infer;
-
-export type AskCodebaseRequest = z.infer;
-export type AskCodebaseResponse = z.infer;
-
-export type LanguageModelInfo = z.infer;
-export type ListLanguageModelsResponse = z.infer;
-
-export type ListTreeApiRequest = z.infer;
-export type ListTreeApiResponse = z.infer;
-export type ListTreeApiNode = ListTreeApiResponse["tree"];
-
-export type ListTreeRequest = z.input;
-export type ListTreeEntry = z.infer;
-export type ListTreeResponse = z.infer;
diff --git a/packages/mcp/src/utils.ts b/packages/mcp/src/utils.ts
deleted file mode 100644
index fc1ee56c3..000000000
--- a/packages/mcp/src/utils.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { ListTreeApiNode, ListTreeEntry, ServiceError } from "./types.js";
-
-
-export const isServiceError = (data: unknown): data is ServiceError => {
- return typeof data === 'object' &&
- data !== null &&
- 'statusCode' in data &&
- 'errorCode' in data &&
- 'message' in data;
-}
-
-export class ServiceErrorException extends Error {
- constructor(public readonly serviceError: ServiceError) {
- super(JSON.stringify(serviceError));
- }
-}
-
-export const normalizeTreePath = (path: string): string => {
- const withoutLeading = path.replace(/^\/+/, '');
- return withoutLeading.replace(/\/+$/, '');
-}
-
-export const joinTreePath = (parentPath: string, name: string): string => {
- if (!parentPath) {
- return name;
- }
-
- return `${parentPath}/${name}`;
-}
-
-export const buildTreeNodeIndex = (root: ListTreeApiNode): Map => {
- const nodeIndex = new Map();
-
- const visit = (node: ListTreeApiNode, currentPath: string) => {
- nodeIndex.set(currentPath, node);
-
- for (const child of node.children) {
- visit(child, joinTreePath(currentPath, child.name));
- }
- };
-
- visit(root, '');
-
- return nodeIndex;
-}
-
-export const sortTreeEntries = (entries: ListTreeEntry[]): ListTreeEntry[] => {
- const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
-
- return [...entries].sort((a, b) => {
- const parentCompare = collator.compare(a.parentPath, b.parentPath);
- if (parentCompare !== 0) {
- return parentCompare;
- }
-
- if (a.type !== b.type) {
- // sort directories above files
- return a.type === 'tree' ? -1 : 1;
- }
-
- const nameCompare = collator.compare(a.name, b.name);
- if (nameCompare !== 0) {
- return nameCompare;
- }
-
- return collator.compare(a.path, b.path);
- });
-}
diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json
deleted file mode 100644
index f84ffe8c9..000000000
--- a/packages/mcp/tsconfig.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "compilerOptions": {
- "outDir": "dist",
- "incremental": true,
- "declaration": true,
- "emitDecoratorMetadata": true,
- "esModuleInterop": true,
- "experimentalDecorators": true,
- "forceConsistentCasingInFileNames": true,
- "isolatedModules": true,
- "module": "Node16",
- "moduleResolution": "Node16",
- "target": "ES2022",
- "noEmitOnError": false,
- "noImplicitAny": true,
- "noUnusedLocals": false,
- "pretty": true,
- "resolveJsonModule": true,
- "skipLibCheck": true,
- "lib": [
- "ES2023"
- ],
- "strict": true,
- "sourceMap": true,
- "inlineSources": true,
- },
- "include": ["src/index.ts"]
-}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 1942f8325..50824bfe9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3889,7 +3889,7 @@ __metadata:
languageName: node
linkType: hard
-"@modelcontextprotocol/sdk@npm:^1.25.0, @modelcontextprotocol/sdk@npm:^1.26.0":
+"@modelcontextprotocol/sdk@npm:^1.25.0":
version: 1.27.1
resolution: "@modelcontextprotocol/sdk@npm:1.27.1"
dependencies:
@@ -9084,26 +9084,6 @@ __metadata:
languageName: unknown
linkType: soft
-"@sourcebot/mcp@workspace:packages/mcp":
- version: 0.0.0-use.local
- resolution: "@sourcebot/mcp@workspace:packages/mcp"
- dependencies:
- "@modelcontextprotocol/sdk": "npm:^1.26.0"
- "@t3-oss/env-core": "npm:^0.13.4"
- "@types/express": "npm:^5.0.1"
- "@types/node": "npm:^20.0.0"
- dedent: "npm:^1.7.1"
- escape-string-regexp: "npm:^5.0.0"
- express: "npm:^5.1.0"
- tsc-watch: "npm:6.2.1"
- tsx: "npm:^4.21.0"
- typescript: "npm:^5.0.0"
- zod: "npm:^4.3.6"
- bin:
- sourcebot-mcp: ./dist/index.js
- languageName: unknown
- linkType: soft
-
"@sourcebot/query-language@workspace:*, @sourcebot/query-language@workspace:packages/queryLanguage":
version: 0.0.0-use.local
resolution: "@sourcebot/query-language@workspace:packages/queryLanguage"
@@ -9424,25 +9404,6 @@ __metadata:
languageName: node
linkType: hard
-"@t3-oss/env-core@npm:^0.13.4":
- version: 0.13.4
- resolution: "@t3-oss/env-core@npm:0.13.4"
- peerDependencies:
- arktype: ^2.1.0
- typescript: ">=5.0.0"
- valibot: ^1.0.0-beta.7 || ^1.0.0
- zod: ^3.24.0 || ^4.0.0-beta.0
- peerDependenciesMeta:
- typescript:
- optional: true
- valibot:
- optional: true
- zod:
- optional: true
- checksum: 10c0/3598c1582b4cd0aead095a492d60cb7656ffa308c0362744fe32f04ec6563601c04d898c0da7b5efb4dc7ace1d3b18a77f268a15a9e940a6997d9dd84f86a749
- languageName: node
- linkType: hard
-
"@tailwindcss/typography@npm:^0.5.16":
version: 0.5.16
resolution: "@tailwindcss/typography@npm:0.5.16"
@@ -9771,7 +9732,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/express@npm:^5.0.0, @types/express@npm:^5.0.1":
+"@types/express@npm:^5.0.0":
version: 5.0.1
resolution: "@types/express@npm:5.0.1"
dependencies:
@@ -9920,15 +9881,6 @@ __metadata:
languageName: node
linkType: hard
-"@types/node@npm:^20.0.0":
- version: 20.17.32
- resolution: "@types/node@npm:20.17.32"
- dependencies:
- undici-types: "npm:~6.19.2"
- checksum: 10c0/2461df36f67704f68db64d33abc5ad00b4b35ac94e996adff88c7322f9572e3e60ddaeed7e9f34ae203120d2ba36cc931fd3a8ddddf0c63943e8600c365c6396
- languageName: node
- linkType: hard
-
"@types/node@npm:^20.17.9":
version: 20.19.37
resolution: "@types/node@npm:20.19.37"
@@ -11369,23 +11321,6 @@ __metadata:
languageName: node
linkType: hard
-"body-parser@npm:^2.2.0":
- version: 2.2.0
- resolution: "body-parser@npm:2.2.0"
- dependencies:
- bytes: "npm:^3.1.2"
- content-type: "npm:^1.0.5"
- debug: "npm:^4.4.0"
- http-errors: "npm:^2.0.0"
- iconv-lite: "npm:^0.6.3"
- on-finished: "npm:^2.4.1"
- qs: "npm:^6.14.0"
- raw-body: "npm:^3.0.0"
- type-is: "npm:^2.0.0"
- checksum: 10c0/a9ded39e71ac9668e2211afa72e82ff86cc5ef94de1250b7d1ba9cc299e4150408aaa5f1e8b03dd4578472a3ce6d1caa2a23b27a6c18e526e48b4595174c116c
- languageName: node
- linkType: hard
-
"body-parser@npm:^2.2.1":
version: 2.2.2
resolution: "body-parser@npm:2.2.2"
@@ -14281,41 +14216,6 @@ __metadata:
languageName: node
linkType: hard
-"express@npm:^5.1.0":
- version: 5.1.0
- resolution: "express@npm:5.1.0"
- dependencies:
- accepts: "npm:^2.0.0"
- body-parser: "npm:^2.2.0"
- content-disposition: "npm:^1.0.0"
- content-type: "npm:^1.0.5"
- cookie: "npm:^0.7.1"
- cookie-signature: "npm:^1.2.1"
- debug: "npm:^4.4.0"
- encodeurl: "npm:^2.0.0"
- escape-html: "npm:^1.0.3"
- etag: "npm:^1.8.1"
- finalhandler: "npm:^2.1.0"
- fresh: "npm:^2.0.0"
- http-errors: "npm:^2.0.0"
- merge-descriptors: "npm:^2.0.0"
- mime-types: "npm:^3.0.0"
- on-finished: "npm:^2.4.1"
- once: "npm:^1.4.0"
- parseurl: "npm:^1.3.3"
- proxy-addr: "npm:^2.0.7"
- qs: "npm:^6.14.0"
- range-parser: "npm:^1.2.1"
- router: "npm:^2.2.0"
- send: "npm:^1.1.0"
- serve-static: "npm:^2.2.0"
- statuses: "npm:^2.0.1"
- type-is: "npm:^2.0.1"
- vary: "npm:^1.1.2"
- checksum: 10c0/80ce7c53c5f56887d759b94c3f2283e2e51066c98d4b72a4cc1338e832b77f1e54f30d0239cc10815a0f849bdb753e6a284d2fa48d4ab56faf9c501f55d751d6
- languageName: node
- linkType: hard
-
"express@npm:^5.2.1":
version: 5.2.1
resolution: "express@npm:5.2.1"
@@ -15540,7 +15440,7 @@ __metadata:
languageName: node
linkType: hard
-"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
+"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
@@ -22442,7 +22342,7 @@ __metadata:
languageName: node
linkType: hard
-"type-is@npm:^2.0.0, type-is@npm:^2.0.1":
+"type-is@npm:^2.0.1":
version: 2.0.1
resolution: "type-is@npm:2.0.1"
dependencies:
@@ -22554,16 +22454,6 @@ __metadata:
languageName: node
linkType: hard
-"typescript@npm:^5.0.0":
- version: 5.8.3
- resolution: "typescript@npm:5.8.3"
- bin:
- tsc: bin/tsc
- tsserver: bin/tsserver
- checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48
- languageName: node
- linkType: hard
-
"typescript@patch:typescript@npm%3A^5#optional!builtin, typescript@patch:typescript@npm%3A^5.6.2#optional!builtin, typescript@patch:typescript@npm%3A^5.7.3#optional!builtin":
version: 5.8.2
resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin::version=5.8.2&hash=5786d5"
@@ -22574,16 +22464,6 @@ __metadata:
languageName: node
linkType: hard
-"typescript@patch:typescript@npm%3A^5.0.0#optional!builtin":
- version: 5.8.3
- resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5"
- bin:
- tsc: bin/tsc
- tsserver: bin/tsserver
- checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb
- languageName: node
- linkType: hard
-
"ua-parser-js@npm:^1.0.33":
version: 1.0.40
resolution: "ua-parser-js@npm:1.0.40"
@@ -23612,7 +23492,7 @@ __metadata:
languageName: node
linkType: hard
-"zod@npm:^3.25 || ^4.0, zod@npm:^3.25.0 || ^4.0.0, zod@npm:^3.25.76 || ^4, zod@npm:^4.1.13, zod@npm:^4.3.6":
+"zod@npm:^3.25 || ^4.0, zod@npm:^3.25.0 || ^4.0.0, zod@npm:^3.25.76 || ^4, zod@npm:^4.1.13":
version: 4.3.6
resolution: "zod@npm:4.3.6"
checksum: 10c0/860d25a81ab41d33aa25f8d0d07b091a04acb426e605f396227a796e9e800c44723ed96d0f53a512b57be3d1520f45bf69c0cb3b378a232a00787a2609625307