diff --git a/CHANGELOG.md b/CHANGELOG.md
index 165c13ad..76ca909a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added special `*` value for `rev:` to allow searching across all branches. [#281](https://github.com/sourcebot-dev/sourcebot/pull/281)
+- Added the Sourcebot Model Context Protocol (MCP) server in [packages/mcp](./packages/mcp/README.md) to allow LLMs to interface with Sourcebot. Checkout the npm package [here](https://www.npmjs.com/package/@sourcebot/mcp). [#292](https://github.com/sourcebot-dev/sourcebot/pull/292)
## [3.1.2] - 2025-04-30
diff --git a/Makefile b/Makefile
index 7b963caa..07bda238 100644
--- a/Makefile
+++ b/Makefile
@@ -29,6 +29,8 @@ clean:
packages/crypto/dist \
packages/error/node_modules \
packages/error/dist \
+ packages/mcp/node_modules \
+ packages/mcp/dist \
.sourcebot
soft-reset:
diff --git a/docs/docs.json b/docs/docs.json
index add8875e..a897fd5e 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -48,7 +48,9 @@
"pages": [
"docs/more/syntax-reference",
"docs/more/multi-branch-indexing",
- "docs/more/roles-and-permissions"
+ "docs/more/roles-and-permissions",
+ "docs/more/mcp-server",
+ "docs/more/search-contexts"
]
}
]
@@ -71,8 +73,7 @@
"self-hosting/more/authentication",
"self-hosting/more/tenancy",
"self-hosting/more/transactional-emails",
- "self-hosting/more/declarative-config",
- "self-hosting/more/search-contexts"
+ "self-hosting/more/declarative-config"
]
},
{
diff --git a/docs/docs/more/mcp-server.mdx b/docs/docs/more/mcp-server.mdx
new file mode 100644
index 00000000..39968add
--- /dev/null
+++ b/docs/docs/more/mcp-server.mdx
@@ -0,0 +1,181 @@
+---
+title: Sourcebot MCP server (@sourcebot/mcp)
+sidebarTitle: Sourcebot MCP server
+---
+
+
+This feature is only available when [self-hosting](/self-hosting) with [authentication](/self-hosting/more/authentication) disabled.
+
+
+The [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) is a open standard for providing context to LLMs. The [@sourcebot/mcp](https://www.npmjs.com/package/@sourcebot/mcp) package is a MCP server that enables LLMs to interface with your Sourcebot instance, enabling MCP clients like Cursor, Vscode, and others to have context over your entire codebase.
+
+## Getting Started
+
+
+
+ Follow the self-hosting [quick start guide](/self-hosting/overview#quick-start-guide) to launch Sourcebot and get your code indexed. The host url of your instance (e.g., `http://localhost:3000`) is passed to the MCP server via the `SOURCEBOT_HOST` url.
+
+ If a host is not provided, then the server will fallback to using the demo instance hosted at https://demo.sourcebot.dev. You can see the list of repositories indexed [here](https://demo.sourcebot.dev/~/repos). Add additional repositories by [opening a PR](https://github.com/sourcebot-dev/sourcebot/blob/main/demo-site-config.json).
+
+
+
+
+ Ensure you have [Node.js](https://nodejs.org/en) >= v18.0.0 installed.
+
+ Next, we can install the [@sourcebot/mcp](https://www.npmjs.com/package/@sourcebot/mcp) MCP server into any supported MCP client:
+
+
+
+ [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" ],
+ "env": {
+ "SOURCEBOT_HOST": "http://localhost:3000"
+ }
+ }
+ }
+ }
+ ```
+
+ Replace `http://localhost:3000` with wherever your Sourcebot instance is hosted.
+
+
+ [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" ],
+ "env": {
+ "SOURCEBOT_HOST": "http://localhost:3000"
+ }
+ }
+ }
+ }
+ ```
+
+ Replace `http://localhost:3000` with wherever your Sourcebot instance is hosted.
+
+
+
+ [VS Code MCP docs](https://code.visualstudio.com/docs/copilot/chat/mcp-servers)
+
+ Add the following to your [settings.json](https://code.visualstudio.com/docs/copilot/chat/mcp-servers):
+
+ ```json
+ {
+ "mcp": {
+ "servers": {
+ "sourcebot": {
+ "type": "stdio",
+ "command": "npx",
+ "args": ["-y", "@sourcebot/mcp@latest"]
+ },
+ "env": {
+ "SOURCEBOT_HOST": "http://localhost:3000"
+ }
+ }
+ }
+ }
+ ```
+
+ Replace `http://localhost:3000` with wherever your Sourcebot instance is hosted.
+
+
+ [Claude Code MCP docs](https://docs.anthropic.com/en/docs/claude-code/tutorials#set-up-model-context-protocol-mcp)
+
+ Run the following command:
+
+ ```sh
+ claude mcp add sourcebot -e SOURCEBOT_HOST=http://localhost:3000 -- npx -y @sourcebot/mcp@latest
+ ```
+
+ Replace `http://localhost:3000` with wherever your Sourcebot instance is hosted.
+
+
+ [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"],
+ "env": {
+ "SOURCEBOT_HOST": "http://localhost:3000"
+ }
+ }
+ }
+ }
+ ```
+
+ Replace `http://localhost:3000` with wherever your Sourcebot instance is hosted.
+
+
+
+
+
+ Tell your LLM to `use sourcebot` when prompting.
+
+
+
+
+
+## Available Tools
+
+
+### `search_code`
+
+Fetches code that matches the provided regex pattern in `query`.
+
+Parameters:
+| Name | Required | Description |
+|:----------------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------|
+| `query` | yes | Regex pattern to search for. Escape special characters and spaces with a single backslash (e.g., 'console\.log', 'console\ log'). |
+| `filterByRepoIds` | no | Restrict search to specific repository IDs (from 'list_repos'). Leave empty to search all. |
+| `filterByLanguages` | no | Restrict search to specific languages (GitHub linguist format, e.g., Python, JavaScript). |
+| `caseSensitive` | no | Case sensitive search (default: false). |
+| `includeCodeSnippets` | no | Include code snippets in results (default: false). |
+| `maxTokens` | no | Max tokens to return (default: env.DEFAULT_MINIMUM_TOKENS). |
+
+
+### `list_repos`
+
+Lists all repositories indexed by Sourcebot.
+
+### `get_file_source`
+
+Fetches the source code for a given file.
+
+Parameters:
+| Name | Required | Description |
+|:-------------|:---------|:-----------------------------------------------------------------|
+| `fileName` | yes | The file to fetch the source code for. |
+| `repoId` | yes | The Sourcebot repository ID. |
+
+
+## Environment Variables
+
+| Name | Default | Description |
+|:-------------------------|:-----------------------|:--------------------------------------------------|
+| `SOURCEBOT_HOST` | http://localhost:3000 | URL of your Sourcebot instance. |
+| `DEFAULT_MINIMUM_TOKENS` | 10000 | Minimum number of tokens to return in responses. |
+| `DEFAULT_MATCHES` | 10000 | Number of code matches to fetch per search. |
+| `DEFAULT_CONTEXT_LINES` | 5 | Lines of context to include above/below matches. |
diff --git a/docs/self-hosting/more/search-contexts.mdx b/docs/docs/more/search-contexts.mdx
similarity index 96%
rename from docs/self-hosting/more/search-contexts.mdx
rename to docs/docs/more/search-contexts.mdx
index 72257048..335c1af1 100644
--- a/docs/self-hosting/more/search-contexts.mdx
+++ b/docs/docs/more/search-contexts.mdx
@@ -6,7 +6,7 @@ sidebarTitle: Search contexts (EE)
import SearchContextSchema from '/snippets/schemas/v3/searchContext.schema.mdx'
-This is only available in the Enterprise Edition. Please add your [license key](/self-hosting/license-key) to activate it.
+This feature is only available when [self-hosting](/self-hosting) with an active Enterprise license. Please add your [license key](/self-hosting/license-key) to activate it.
A **search context** is a user-defined grouping of repositories that helps focus searches on specific areas of your codebase, like frontend, backend, or infrastructure code. Some example queries using search contexts:
diff --git a/package.json b/package.json
index f9b8ba68..e4781586 100644
--- a/package.json
+++ b/package.json
@@ -6,11 +6,13 @@
"scripts": {
"build": "cross-env SKIP_ENV_VALIDATION=1 yarn workspaces foreach -A run build",
"test": "yarn workspaces foreach -A run test",
- "dev": "yarn dev:prisma:migrate:dev && npm-run-all --print-label --parallel dev:zoekt dev:backend dev:web",
+ "dev": "yarn dev:prisma:migrate:dev && npm-run-all --print-label --parallel dev:zoekt dev:backend dev:web watch:mcp watch:schemas",
"with-env": "cross-env PATH=\"$PWD/bin:$PATH\" dotenv -e .env.development -c --",
"dev:zoekt": "yarn with-env zoekt-webserver -index .sourcebot/index -rpc",
"dev:backend": "yarn with-env yarn workspace @sourcebot/backend dev:watch",
"dev:web": "yarn with-env yarn workspace @sourcebot/web dev",
+ "watch:mcp": "yarn workspace @sourcebot/mcp build:watch",
+ "watch:schemas": "yarn workspace @sourcebot/schemas watch",
"dev:prisma:migrate:dev": "yarn with-env yarn workspace @sourcebot/db prisma:migrate:dev",
"dev:prisma:studio": "yarn with-env yarn workspace @sourcebot/db prisma:studio",
"dev:prisma:migrate:reset": "yarn with-env yarn workspace @sourcebot/db prisma:migrate:reset",
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 9005a7e1..e3fef75a 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -51,6 +51,6 @@
"simple-git": "^3.27.0",
"strip-json-comments": "^5.0.1",
"winston": "^3.15.0",
- "zod": "^3.24.2"
+ "zod": "^3.24.3"
}
}
diff --git a/packages/mcp/.gitignore b/packages/mcp/.gitignore
new file mode 100644
index 00000000..23bfe49c
--- /dev/null
+++ b/packages/mcp/.gitignore
@@ -0,0 +1,2 @@
+dist/
+node_modules/
\ No newline at end of file
diff --git a/packages/mcp/.npmignore b/packages/mcp/.npmignore
new file mode 100644
index 00000000..f09a7667
--- /dev/null
+++ b/packages/mcp/.npmignore
@@ -0,0 +1,5 @@
+**/*
+!/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
new file mode 100644
index 00000000..f4f3b377
--- /dev/null
+++ b/packages/mcp/CHANGELOG.md
@@ -0,0 +1,13 @@
+# 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.0] - 2025-05-07
+
+### Added
+- Initial release
\ No newline at end of file
diff --git a/packages/mcp/README.md b/packages/mcp/README.md
new file mode 100644
index 00000000..9b3e9043
--- /dev/null
+++ b/packages/mcp/README.md
@@ -0,0 +1,219 @@
+# Sourcebot MCP - Blazingly fast agentic code search for GitHub, GitLab, BitBucket, and more
+
+[](https://sourcebot.dev)
+[](https://github.com/sourcebot-dev/sourcebot)
+[](https://docs.sourcebot.dev/docs/more/mcp-server)
+[](https://www.npmjs.com/package/@sourcebot/mcp)
+
+
+The Sourcebot MCP server enables precise regular expression code search across 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). This unlocks the capability for LLM agents to fetch code context for repositories that aren't checked out locally. Some use cases where precise search on a wider code context can help:
+
+- 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 depreacted 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.
+
+ If a host is not provided, then the server will fallback to using the demo instance hosted at https://demo.sourcebot.dev. You can see the list of repositories indexed [here](https://demo.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://demo.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://demo.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 [settings.json](https://code.visualstudio.com/docs/copilot/chat/mcp-servers):
+
+ ```json
+ {
+ "mcp": {
+ "servers": {
+ "sourcebot": {
+ "type": "stdio",
+ "command": "npx",
+ "args": ["-y", "@sourcebot/mcp@latest"],
+ // Optional - if not specified, https://demo.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://demo.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://demo.sourcebot.dev is used
+ "env": {
+ "SOURCEBOT_HOST": "http://localhost:3000"
+ }
+ }
+ }
+ }
+ ```
+
+
+
+4. Tell your LLM to `use sourcebot` when prompting.
+
+
+
+For a more detailed guide, checkout [the docs](https://docs.sourcebot.dev/docs/more/mcp-server).
+
+
+## Available Tools
+
+### search_code
+
+Fetches code that matches the provided regex pattern in `query`.
+
+
+Parameters
+
+| Name | Required | Description |
+|:----------------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------|
+| `query` | yes | Regex pattern to search for. Escape special characters and spaces with a single backslash (e.g., 'console\.log', 'console\ log'). |
+| `filterByRepoIds` | no | Restrict search to specific repository IDs (from 'list_repos'). Leave empty to search all. |
+| `filterByLanguages` | no | Restrict search to specific languages (GitHub linguist format, e.g., Python, JavaScript). |
+| `caseSensitive` | no | Case sensitive search (default: false). |
+| `includeCodeSnippets` | no | Include code snippets in results (default: false). |
+| `maxTokens` | no | Max tokens to return (default: env.DEFAULT_MINIMUM_TOKENS). |
+
+
+
+### list_repos
+
+Lists all repositories indexed by Sourcebot.
+
+### get_file_source
+
+Fetches the source code for a given file.
+
+
+Parameters
+
+| Name | Required | Description |
+|:-------------|:---------|:-----------------------------------------------------------------|
+| `fileName` | yes | The file to fetch the source code for. |
+| `repoId` | yes | The Sourcebot repository ID. |
+
+
+
+## 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 [GitHub discussion](https://github.com/sourcebot-dev/sourcebot/discussions/categories/ideas).
+
+## 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)
+
+### Code Navigation
+
+Another idea is to allow LLMs to traverse abstract syntax trees (ASTs) of a codebase to enable reliable code navigation. This could be packaged as tools like `goto_definition`, `find_all_references`, etc., which could be useful for LLMs to get additional code context. [GitHub Discussion](https://github.com/sourcebot-dev/sourcebot/discussions/296)
+
+### Got an idea?
+
+Open up a [GitHub discussion](https://github.com/sourcebot-dev/sourcebot/discussions/categories/feature-requests)!
diff --git a/packages/mcp/package.json b/packages/mcp/package.json
new file mode 100644
index 00000000..351ea9c6
--- /dev/null
+++ b/packages/mcp/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@sourcebot/mcp",
+ "version": "1.0.0",
+ "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.0.0",
+ "typescript": "^5.0.0"
+ },
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.10.2",
+ "@t3-oss/env-core": "^0.13.4",
+ "escape-string-regexp": "^5.0.0",
+ "express": "^5.1.0",
+ "zod": "^3.24.3"
+ },
+ "bin": {
+ "sourcebot-mcp": "./dist/index.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/sourcebot-dev/sourcebot.git",
+ "directory": "packages/mcp"
+ },
+ "keywords": [
+ "mcp",
+ "modelcontextprotocol",
+ "code-search",
+ "sourcebot",
+ "code-intelligence"
+ ]
+}
\ No newline at end of file
diff --git a/packages/mcp/src/client.ts b/packages/mcp/src/client.ts
new file mode 100644
index 00000000..bcb0fd8e
--- /dev/null
+++ b/packages/mcp/src/client.ts
@@ -0,0 +1,55 @@
+import { env } from './env.js';
+import { listRepositoriesResponseSchema, searchResponseSchema, fileSourceResponseSchema } from './schemas.js';
+import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse, ServiceError } from './types.js';
+import { isServiceError } from './utils.js';
+
+export const search = async (request: SearchRequest): Promise => {
+ console.error(`Executing search request: ${JSON.stringify(request, null, 2)}`);
+ const result = await fetch(`${env.SOURCEBOT_HOST}/api/search`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Org-Domain': '~'
+ },
+ body: JSON.stringify(request)
+ }).then(response => response.json());
+
+ if (isServiceError(result)) {
+ return result;
+ }
+
+ return searchResponseSchema.parse(result);
+}
+
+export const listRepos = async (): Promise => {
+ const result = await fetch(`${env.SOURCEBOT_HOST}/api/repos`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Org-Domain': '~'
+ },
+ }).then(response => response.json());
+
+ if (isServiceError(result)) {
+ return result;
+ }
+
+ return listRepositoriesResponseSchema.parse(result);
+}
+
+export const getFileSource = async (request: FileSourceRequest): Promise => {
+ const result = await fetch(`${env.SOURCEBOT_HOST}/api/source`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Org-Domain': '~'
+ },
+ body: JSON.stringify(request)
+ }).then(response => response.json());
+
+ if (isServiceError(result)) {
+ return result;
+ }
+
+ return fileSourceResponseSchema.parse(result);
+}
diff --git a/packages/mcp/src/env.ts b/packages/mcp/src/env.ts
new file mode 100644
index 00000000..35559f7d
--- /dev/null
+++ b/packages/mcp/src/env.ts
@@ -0,0 +1,23 @@
+import { createEnv } from "@t3-oss/env-core";
+import { z } from "zod";
+
+export const numberSchema = z.coerce.number();
+
+const SOURCEBOT_DEMO_HOST = "https://demo.sourcebot.dev";
+
+export const env = createEnv({
+ server: {
+ SOURCEBOT_HOST: z.string().url().default(SOURCEBOT_DEMO_HOST),
+
+ // 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
new file mode 100644
index 00000000..7e36f50e
--- /dev/null
+++ b/packages/mcp/src/index.ts
@@ -0,0 +1,223 @@
+#!/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 escapeStringRegexp from 'escape-string-regexp';
+import { z } from 'zod';
+import { listRepos, search, getFileSource } from './client.js';
+import { env, numberSchema } from './env.js';
+import { TextContent } from './types.js';
+import { base64Decode, isServiceError } from './utils.js';
+
+// Create MCP server
+const server = new McpServer({
+ name: 'sourcebot-mcp-server',
+ version: '0.1.0',
+});
+
+
+server.tool(
+ "search_code",
+ `Fetches code that matches the provided regex pattern in \`query\`. This is NOT a semantic search.
+ Results are returned as an array of matching files, with the file's URL, repository, and language.
+ If the \`includeCodeSnippets\` property is true, code snippets containing the matches will be included in the response. Only set this to true if the request requires code snippets (e.g., show me examples where library X is used).
+ When referencing a file in your response, **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.
+ **ONLY USE** the \`filterByRepoIds\` property if the request requires searching a specific repo(s). Otherwise, leave it empty.`,
+ {
+ query: z
+ .string()
+ .describe(`The regex pattern to search for. RULES:
+ 1. When a regex special character needs to be escaped, ALWAYS use a single backslash (\) (e.g., 'console\.log')
+ 2. **ALWAYS** escape spaces with a single backslash (\) (e.g., 'console\ log')
+ `),
+ filterByRepoIds: z
+ .array(z.string())
+ .describe(`Scope the search to the provided repositories to the Sourcebot compatible repository IDs. **DO NOT** use this property if you want to search all repositories. **YOU MUST** call 'list_repos' first to obtain the exact repository ID.`)
+ .optional(),
+ filterByLanguages: z
+ .array(z.string())
+ .describe(`Scope the search to the provided languages. The language MUST be formatted as a GitHub linguist language. Examples: Python, JavaScript, TypeScript, Java, C#, C++, PHP, Go, Rust, Ruby, Swift, Kotlin, Shell, C, Dart, HTML, CSS, PowerShell, SQL, R`)
+ .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 (default: false). If false, only the file's URL, repository, and language will be returned. Set to false to get a more concise response.`)
+ .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(),
+ },
+ async ({
+ query,
+ filterByRepoIds: repoIds = [],
+ filterByLanguages: languages = [],
+ maxTokens = env.DEFAULT_MINIMUM_TOKENS,
+ includeCodeSnippets = false,
+ caseSensitive = false,
+ }) => {
+ if (repoIds.length > 0) {
+ query += ` ( repo:${repoIds.map(id => escapeStringRegexp(id)).join(' or repo:')} )`;
+ }
+
+ if (languages.length > 0) {
+ query += ` ( lang:${languages.join(' or lang:')} )`;
+ }
+
+ if (caseSensitive) {
+ query += ` case:yes`;
+ } else {
+ query += ` case:no`;
+ }
+
+ console.error(`Executing search request: ${query}`);
+
+ const response = await search({
+ query,
+ matches: env.DEFAULT_MATCHES,
+ contextLines: env.DEFAULT_CONTEXT_LINES,
+ });
+
+ if (isServiceError(response)) {
+ return {
+ content: [{
+ type: "text",
+ text: `Error searching code: ${response.message}`,
+ }],
+ };
+ }
+
+ 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 = `file: ${file.url}\nnum_matches: ${numMatches}\nrepository: ${file.repository}\nlanguage: ${file.language}`;
+
+ if (includeCodeSnippets) {
+ const snippets = file.chunks.map(chunk => {
+ const content = base64Decode(chunk.content);
+ return `\`\`\`\n${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) {
+ 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_repos",
+ "Lists all repositories in the organization.",
+ async () => {
+ const response = await listRepos();
+ if (isServiceError(response)) {
+ return {
+ content: [{
+ type: "text",
+ text: `Error listing repositories: ${response.message}`,
+ }],
+ };
+ }
+
+ const content: TextContent[] = response.repos.map(repo => {
+ return {
+ type: "text",
+ text: `id: ${repo.name}\nurl: ${repo.url}`,
+ }
+ });
+
+ return {
+ content,
+ };
+ }
+);
+
+server.tool(
+ "get_file_source",
+ "Fetches the source code for a given file.",
+ {
+ fileName: z.string().describe("The file to fetch the source code for."),
+ repoId: z.string().describe("The repository to fetch the source code for. This is the Sourcebot compatible repository ID."),
+ },
+ async ({ fileName, repoId }) => {
+ const response = await getFileSource({
+ fileName,
+ repository: repoId,
+ });
+
+ if (isServiceError(response)) {
+ return {
+ content: [{
+ type: "text",
+ text: `Error fetching file source: ${response.message}`,
+ }],
+ };
+ }
+
+ const content: TextContent[] = [{
+ type: "text",
+ text: `file: ${fileName}\nrepository: ${repoId}\nlanguage: ${response.language}\nsource:\n${base64Decode(response.source)}`,
+ }]
+
+ return {
+ content,
+ };
+ }
+);
+
+
+
+const runServer = async () => {
+ const transport = new StdioServerTransport();
+ await server.connect(transport);
+ console.error('Sourcebot MCP server ready');
+}
+
+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
new file mode 100644
index 00000000..6bf02c7e
--- /dev/null
+++ b/packages/mcp/src/schemas.ts
@@ -0,0 +1,111 @@
+// @NOTE : Please keep this file in sync with @sourcebot/web/src/features/search/schemas.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 searchRequestSchema = z.object({
+ // The zoekt query to execute.
+ query: z.string(),
+ // The number of matches to return.
+ matches: z.number(),
+ // The number of context lines to return.
+ contextLines: z.number().optional(),
+ // Whether to return the whole file as part of the response.
+ whole: z.boolean().optional(),
+});
+
+export const searchResponseSchema = z.object({
+ zoektStats: z.object({
+ // The duration (in nanoseconds) of the search.
+ duration: z.number(),
+ fileCount: z.number(),
+ matchCount: z.number(),
+ filesSkipped: z.number(),
+ contentBytesLoaded: z.number(),
+ indexBytesLoaded: z.number(),
+ crashes: z.number(),
+ shardFilesConsidered: z.number(),
+ filesConsidered: z.number(),
+ filesLoaded: z.number(),
+ shardsScanned: z.number(),
+ shardsSkipped: z.number(),
+ shardsSkippedFilter: z.number(),
+ ngramMatches: z.number(),
+ ngramLookups: z.number(),
+ wait: z.number(),
+ matchTreeConstruction: z.number(),
+ matchTreeSearch: z.number(),
+ regexpsConsidered: z.number(),
+ flushReason: z.number(),
+ }),
+ files: z.array(z.object({
+ fileName: z.object({
+ // The name of the file
+ text: z.string(),
+ // Any matching ranges
+ matchRanges: z.array(rangeSchema),
+ }),
+ repository: z.string(),
+ language: z.string(),
+ url: 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(),
+ })),
+ isBranchFilteringEnabled: z.boolean(),
+});
+
+export const repositorySchema = z.object({
+ name: z.string(),
+ url: z.string(),
+ branches: z.array(z.string()),
+ rawConfig: z.record(z.string(), z.string()).optional(),
+});
+
+export const listRepositoriesResponseSchema = z.object({
+ repos: z.array(repositorySchema),
+});
+
+export const fileSourceRequestSchema = z.object({
+ fileName: z.string(),
+ repository: z.string(),
+ branch: z.string().optional(),
+});
+
+export const fileSourceResponseSchema = z.object({
+ source: z.string(),
+ language: z.string(),
+});
+
+export const serviceErrorSchema = z.object({
+ statusCode: z.number(),
+ errorCode: z.string(),
+ message: z.string(),
+});
diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts
new file mode 100644
index 00000000..f789c8c1
--- /dev/null
+++ b/packages/mcp/src/types.ts
@@ -0,0 +1,32 @@
+// @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,
+ listRepositoriesResponseSchema,
+ locationSchema,
+ searchRequestSchema,
+ searchResponseSchema,
+ rangeSchema,
+ fileSourceRequestSchema,
+ symbolSchema,
+ serviceErrorSchema,
+} 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 ListRepositoriesResponse = z.infer;
+export type Repository = ListRepositoriesResponse["repos"][number];
+
+export type FileSourceRequest = z.infer;
+export type FileSourceResponse = z.infer;
+
+export type TextContent = { type: "text", text: string };
+
+export type ServiceError = z.infer;
diff --git a/packages/mcp/src/utils.ts b/packages/mcp/src/utils.ts
new file mode 100644
index 00000000..a99114c3
--- /dev/null
+++ b/packages/mcp/src/utils.ts
@@ -0,0 +1,15 @@
+import { ServiceError } from "./types.js";
+
+// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
+export const base64Decode = (base64: string): string => {
+ const binString = atob(base64);
+ return Buffer.from(Uint8Array.from(binString, (m) => m.codePointAt(0)!).buffer).toString();
+}
+
+export const isServiceError = (data: unknown): data is ServiceError => {
+ return typeof data === 'object' &&
+ data !== null &&
+ 'statusCode' in data &&
+ 'errorCode' in data &&
+ 'message' in data;
+}
\ No newline at end of file
diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json
new file mode 100644
index 00000000..f84ffe8c
--- /dev/null
+++ b/packages/mcp/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "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/packages/schemas/package.json b/packages/schemas/package.json
index 01fadaee..632361fd 100644
--- a/packages/schemas/package.json
+++ b/packages/schemas/package.json
@@ -5,12 +5,14 @@
"scripts": {
"build": "yarn generate && tsc",
"generate": "tsx tools/generate.ts",
+ "watch": "nodemon --watch ../../schemas -e json -x 'yarn generate'",
"postinstall": "yarn build"
},
"devDependencies": {
"@apidevtools/json-schema-ref-parser": "^11.7.3",
"glob": "^11.0.1",
"json-schema-to-typescript": "^15.0.4",
+ "nodemon": "^3.1.10",
"tsx": "^4.19.2",
"typescript": "^5.7.3"
},
diff --git a/packages/web/package.json b/packages/web/package.json
index 769b0e91..a652f59e 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -135,7 +135,7 @@
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.1.0",
- "zod": "^3.24.2"
+ "zod": "^3.24.3"
},
"devDependencies": {
"@types/micromatch": "^4.0.9",
diff --git a/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx b/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx
index 97218b3d..0d20b9a6 100644
--- a/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx
+++ b/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx
@@ -13,7 +13,6 @@ interface CodePreviewPanelProps {
onClose: () => void;
selectedMatchIndex: number;
onSelectedMatchIndexChange: (index: number) => void;
- repoUrlTemplates: Record;
}
export const CodePreviewPanel = ({
@@ -21,7 +20,6 @@ export const CodePreviewPanel = ({
onClose,
selectedMatchIndex,
onSelectedMatchIndexChange,
- repoUrlTemplates,
}: CodePreviewPanelProps) => {
const domain = useDomain();
@@ -42,45 +40,13 @@ export const CodePreviewPanel = ({
branch,
}, domain)
.then(({ source }) => {
- const link = (() => {
- const template = repoUrlTemplates[fileMatch.repository];
-
- // This is a hacky parser for templates generated by
- // the go text/template package. Example template:
- // {{URLJoinPath "https://github.com/sourcebot-dev/sourcebot" "blob" .Version .Path}}
- // @see: https://pkg.go.dev/text/template
- if (!template || !template.match(/^{{URLJoinPath\s.*}}(\?.+)?$/)) {
- return undefined;
- }
-
- const url =
- template.substring("{{URLJoinPath ".length,template.indexOf("}}"))
- .replace(".Version", branch ?? "HEAD")
- .replace(".Path", fileMatch.fileName.text)
- .split(" ")
- .map((part) => {
- // remove wrapping quotes
- if (part.startsWith("\"")) part = part.substring(1);
- if (part.endsWith("\"")) part = part.substring(0, part.length - 1);
- return part;
- })
- .join("/");
-
- const optionalQueryParams =
- template.substring(template.indexOf("}}") + 2)
- .replace("{{.Version}}", branch ?? "HEAD")
- .replace("{{.Path}}", fileMatch.fileName.text);
-
- return url + optionalQueryParams;
- })();
-
const decodedSource = base64Decode(source);
return {
content: decodedSource,
filepath: fileMatch.fileName.text,
matches: fileMatch.chunks,
- link: link,
+ link: fileMatch.url,
language: fileMatch.language,
revision: branch ?? "HEAD",
};
diff --git a/packages/web/src/app/[domain]/search/page.tsx b/packages/web/src/app/[domain]/search/page.tsx
index f3aa90c5..f719d2b0 100644
--- a/packages/web/src/app/[domain]/search/page.tsx
+++ b/packages/web/src/app/[domain]/search/page.tsx
@@ -141,14 +141,13 @@ const SearchPageInternal = () => {
});
}, [captureEvent, searchQuery, searchResponse]);
- const { fileMatches, searchDurationMs, totalMatchCount, isBranchFilteringEnabled, repoUrlTemplates } = useMemo(() => {
+ const { fileMatches, searchDurationMs, totalMatchCount, isBranchFilteringEnabled } = useMemo(() => {
if (!searchResponse) {
return {
fileMatches: [],
searchDurationMs: 0,
totalMatchCount: 0,
isBranchFilteringEnabled: false,
- repoUrlTemplates: {},
};
}
@@ -157,7 +156,6 @@ const SearchPageInternal = () => {
searchDurationMs: Math.round(searchResponse.durationMs),
totalMatchCount: searchResponse.zoektStats.matchCount,
isBranchFilteringEnabled: searchResponse.isBranchFilteringEnabled,
- repoUrlTemplates: searchResponse.repoUrlTemplates,
}
}, [searchResponse]);
@@ -207,7 +205,6 @@ const SearchPageInternal = () => {
isMoreResultsButtonVisible={isMoreResultsButtonVisible}
onLoadMoreResults={onLoadMoreResults}
isBranchFilteringEnabled={isBranchFilteringEnabled}
- repoUrlTemplates={repoUrlTemplates}
repoMetadata={repoMetadata ?? {}}
searchDurationMs={searchDurationMs}
numMatches={numMatches}
@@ -222,7 +219,6 @@ interface PanelGroupProps {
isMoreResultsButtonVisible?: boolean;
onLoadMoreResults: () => void;
isBranchFilteringEnabled: boolean;
- repoUrlTemplates: Record;
repoMetadata: Record;
searchDurationMs: number;
numMatches: number;
@@ -233,7 +229,6 @@ const PanelGroup = ({
isMoreResultsButtonVisible,
onLoadMoreResults,
isBranchFilteringEnabled,
- repoUrlTemplates,
repoMetadata,
searchDurationMs,
numMatches,
@@ -340,7 +335,6 @@ const PanelGroup = ({
onClose={() => setSelectedFile(undefined)}
selectedMatchIndex={selectedMatchIndex}
onSelectedMatchIndexChange={setSelectedMatchIndex}
- repoUrlTemplates={repoUrlTemplates}
/>
diff --git a/packages/web/src/features/search/listReposApi.ts b/packages/web/src/features/search/listReposApi.ts
index 7baae79c..fb0a0318 100644
--- a/packages/web/src/features/search/listReposApi.ts
+++ b/packages/web/src/features/search/listReposApi.ts
@@ -34,7 +34,6 @@ export const listRepositories = async (orgId: number): Promise ({
name: repo.Repository.Name,
url: repo.Repository.URL,
- source: repo.Repository.Source,
branches: repo.Repository.Branches?.map((branch) => branch.Name) ?? [],
rawConfig: repo.Repository.RawConfig ?? undefined,
}))
diff --git a/packages/web/src/features/search/schemas.ts b/packages/web/src/features/search/schemas.ts
index 0b087cb0..a4305c00 100644
--- a/packages/web/src/features/search/schemas.ts
+++ b/packages/web/src/features/search/schemas.ts
@@ -1,3 +1,4 @@
+// @NOTE : Please keep this file in sync with @sourcebot/mcp/src/schemas.ts
import { z } from "zod";
export const locationSchema = z.object({
@@ -61,6 +62,7 @@ export const searchResponseSchema = z.object({
// Any matching ranges
matchRanges: z.array(rangeSchema),
}),
+ url: z.string(),
repository: z.string(),
language: z.string(),
chunks: z.array(z.object({
@@ -76,14 +78,12 @@ export const searchResponseSchema = z.object({
// Set if `whole` is true.
content: z.string().optional(),
})),
- repoUrlTemplates: z.record(z.string(), z.string()),
isBranchFilteringEnabled: z.boolean(),
});
export const repositorySchema = z.object({
name: z.string(),
url: z.string(),
- source: z.string(),
branches: z.array(z.string()),
rawConfig: z.record(z.string(), z.string()).optional(),
});
diff --git a/packages/web/src/features/search/searchApi.ts b/packages/web/src/features/search/searchApi.ts
index eb055b50..6ba75091 100644
--- a/packages/web/src/features/search/searchApi.ts
+++ b/packages/web/src/features/search/searchApi.ts
@@ -7,6 +7,7 @@ import { ErrorCode } from "../../lib/errorCodes";
import { StatusCodes } from "http-status-codes";
import { zoektSearchResponseSchema } from "./zoektSchema";
import { SearchRequest, SearchResponse, SearchResultRange } from "./types";
+import assert from "assert";
// List of supported query prefixes in zoekt.
// @see : https://github.com/sourcebot-dev/zoekt/blob/main/query/parse.go#L417
@@ -90,6 +91,36 @@ const transformZoektQuery = async (query: string, orgId: number): Promise {
+ // remove wrapping quotes
+ if (part.startsWith("\"")) part = part.substring(1);
+ if (part.endsWith("\"")) part = part.substring(0, part.length - 1);
+ return part;
+ })
+ .join("/");
+
+ const optionalQueryParams =
+ template.substring(template.indexOf("}}") + 2)
+ .replace("{{.Version}}", branch)
+ .replace("{{.Path}}", fileName);
+
+ return encodeURI(url + optionalQueryParams);
+}
+
export const search = async ({ query, matches, contextLines, whole }: SearchRequest, orgId: number) => {
const transformedQuery = await transformZoektQuery(query, orgId);
if (isServiceError(transformedQuery)) {
@@ -165,6 +196,15 @@ export const search = async ({ query, matches, contextLines, whole }: SearchRequ
},
files: Result.Files?.map((file) => {
const fileNameChunks = file.ChunkMatches.filter((chunk) => chunk.FileName);
+
+ const template = Result.RepoURLs[file.Repository];
+ assert(template, `Template not found for repository ${file.Repository}`);
+
+ // If there are multiple branches pointing to the same revision of this file, it doesn't
+ // matter which branch we use here, so use the first one.
+ const branch = file.Branches && file.Branches.length > 0 ? file.Branches[0] : "HEAD";
+ const url = getRepositoryUrl(template, branch, file.FileName);
+
return {
fileName: {
text: file.FileName,
@@ -182,6 +222,7 @@ export const search = async ({ query, matches, contextLines, whole }: SearchRequ
})) : [],
},
repository: file.Repository,
+ url: url,
language: file.Language,
chunks: file.ChunkMatches
.filter((chunk) => !chunk.FileName) // Filter out filename chunks.
@@ -220,9 +261,7 @@ export const search = async ({ query, matches, contextLines, whole }: SearchRequ
branches: file.Branches,
content: file.Content,
}
- }
- ) ?? [],
- repoUrlTemplates: Result.RepoURLs,
+ }) ?? [],
isBranchFilteringEnabled: isBranchFilteringEnabled,
} satisfies SearchResponse));
diff --git a/packages/web/src/features/search/types.ts b/packages/web/src/features/search/types.ts
index 1f652ee4..ccf2c32f 100644
--- a/packages/web/src/features/search/types.ts
+++ b/packages/web/src/features/search/types.ts
@@ -1,3 +1,4 @@
+// @NOTE : Please keep this file in sync with @sourcebot/mcp/src/types.ts
import {
fileSourceResponseSchema,
listRepositoriesResponseSchema,
diff --git a/yarn.lock b/yarn.lock
index af3c85c0..992e8954 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1970,6 +1970,24 @@ __metadata:
languageName: node
linkType: hard
+"@modelcontextprotocol/sdk@npm:^1.10.2":
+ version: 1.10.2
+ resolution: "@modelcontextprotocol/sdk@npm:1.10.2"
+ dependencies:
+ content-type: "npm:^1.0.5"
+ cors: "npm:^2.8.5"
+ cross-spawn: "npm:^7.0.3"
+ eventsource: "npm:^3.0.2"
+ express: "npm:^5.0.1"
+ express-rate-limit: "npm:^7.5.0"
+ pkce-challenge: "npm:^5.0.0"
+ raw-body: "npm:^3.0.0"
+ zod: "npm:^3.23.8"
+ zod-to-json-schema: "npm:^3.24.1"
+ checksum: 10c0/a2a146dec37d13c8108b4c42912d65f1f5b0e8f3adda4c300336369519f3caa52e996afb65d6a6c03ae3b6fc1e2425cad4af1e619206b6ee3e15327b4ee01d4c
+ languageName: node
+ linkType: hard
+
"@msgpack/msgpack@npm:^2.5.1":
version: 2.8.0
resolution: "@msgpack/msgpack@npm:2.8.0"
@@ -5188,7 +5206,7 @@ __metadata:
typescript: "npm:^5.6.2"
vitest: "npm:^2.1.9"
winston: "npm:^3.15.0"
- zod: "npm:^3.24.2"
+ zod: "npm:^3.24.3"
languageName: unknown
linkType: soft
@@ -5226,6 +5244,25 @@ __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.10.2"
+ "@t3-oss/env-core": "npm:^0.13.4"
+ "@types/express": "npm:^5.0.1"
+ "@types/node": "npm:^20.0.0"
+ escape-string-regexp: "npm:^5.0.0"
+ express: "npm:^5.1.0"
+ tsc-watch: "npm:6.2.1"
+ tsx: "npm:^4.0.0"
+ typescript: "npm:^5.0.0"
+ zod: "npm:^3.24.3"
+ bin:
+ sourcebot-mcp: ./dist/index.js
+ languageName: unknown
+ linkType: soft
+
"@sourcebot/schemas@workspace:*, @sourcebot/schemas@workspace:packages/schemas":
version: 0.0.0-use.local
resolution: "@sourcebot/schemas@workspace:packages/schemas"
@@ -5233,6 +5270,7 @@ __metadata:
"@apidevtools/json-schema-ref-parser": "npm:^11.7.3"
glob: "npm:^11.0.1"
json-schema-to-typescript: "npm:^15.0.4"
+ nodemon: "npm:^3.1.10"
tsx: "npm:^4.19.2"
typescript: "npm:^5.7.3"
languageName: unknown
@@ -5387,7 +5425,7 @@ __metadata:
usehooks-ts: "npm:^3.1.0"
vite-tsconfig-paths: "npm:^5.1.3"
vitest: "npm:^2.1.5"
- zod: "npm:^3.24.2"
+ zod: "npm:^3.24.3"
languageName: unknown
linkType: soft
@@ -5457,6 +5495,25 @@ __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
+
"@t3-oss/env-nextjs@npm:^0.12.0":
version: 0.12.0
resolution: "@t3-oss/env-nextjs@npm:0.12.0"
@@ -5610,7 +5667,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/express@npm:^5.0.0":
+"@types/express@npm:^5.0.0, @types/express@npm:^5.0.1":
version: 5.0.1
resolution: "@types/express@npm:5.0.1"
dependencies:
@@ -5710,6 +5767,15 @@ __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/nodemailer@npm:^6.4.17":
version: 6.4.17
resolution: "@types/nodemailer@npm:6.4.17"
@@ -6298,6 +6364,16 @@ __metadata:
languageName: node
linkType: hard
+"accepts@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "accepts@npm:2.0.0"
+ dependencies:
+ mime-types: "npm:^3.0.0"
+ negotiator: "npm:^1.0.0"
+ checksum: 10c0/98374742097e140891546076215f90c32644feacf652db48412329de4c2a529178a81aa500fbb13dd3e6cbf6e68d829037b123ac037fc9a08bcec4b87b358eef
+ languageName: node
+ linkType: hard
+
"accepts@npm:~1.3.4, accepts@npm:~1.3.8":
version: 1.3.8
resolution: "accepts@npm:1.3.8"
@@ -6738,6 +6814,23 @@ __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
+
"brace-expansion@npm:^1.1.7":
version: 1.1.11
resolution: "brace-expansion@npm:1.1.11"
@@ -6814,7 +6907,7 @@ __metadata:
languageName: node
linkType: hard
-"bytes@npm:3.1.2":
+"bytes@npm:3.1.2, bytes@npm:^3.1.2":
version: 3.1.2
resolution: "bytes@npm:3.1.2"
checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e
@@ -6973,7 +7066,7 @@ __metadata:
languageName: node
linkType: hard
-"chokidar@npm:^3.5.3, chokidar@npm:^3.6.0":
+"chokidar@npm:^3.5.2, chokidar@npm:^3.5.3, chokidar@npm:^3.6.0":
version: 3.6.0
resolution: "chokidar@npm:3.6.0"
dependencies:
@@ -7447,7 +7540,16 @@ __metadata:
languageName: node
linkType: hard
-"content-type@npm:~1.0.4, content-type@npm:~1.0.5":
+"content-disposition@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "content-disposition@npm:1.0.0"
+ dependencies:
+ safe-buffer: "npm:5.2.1"
+ checksum: 10c0/c7b1ba0cea2829da0352ebc1b7f14787c73884bc707c8bc2271d9e3bf447b372270d09f5d3980dc5037c749ceef56b9a13fccd0b0001c87c3f12579967e4dd27
+ languageName: node
+ linkType: hard
+
+"content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5":
version: 1.0.5
resolution: "content-type@npm:1.0.5"
checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af
@@ -7468,6 +7570,13 @@ __metadata:
languageName: node
linkType: hard
+"cookie-signature@npm:^1.2.1":
+ version: 1.2.2
+ resolution: "cookie-signature@npm:1.2.2"
+ checksum: 10c0/54e05df1a293b3ce81589b27dddc445f462f6fa6812147c033350cd3561a42bc14481674e05ed14c7bd0ce1e8bb3dc0e40851bad75415733711294ddce0b7bc6
+ languageName: node
+ linkType: hard
+
"cookie@npm:0.7.1":
version: 0.7.1
resolution: "cookie@npm:0.7.1"
@@ -7475,7 +7584,7 @@ __metadata:
languageName: node
linkType: hard
-"cookie@npm:~0.7.2":
+"cookie@npm:^0.7.1, cookie@npm:~0.7.2":
version: 0.7.2
resolution: "cookie@npm:0.7.2"
checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2
@@ -7489,7 +7598,7 @@ __metadata:
languageName: node
linkType: hard
-"cors@npm:~2.8.5":
+"cors@npm:^2.8.5, cors@npm:~2.8.5":
version: 2.8.5
resolution: "cors@npm:2.8.5"
dependencies:
@@ -7659,7 +7768,7 @@ __metadata:
languageName: node
linkType: hard
-"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.7, debug@npm:^4.4.0":
+"debug@npm:4, debug@npm:^4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.7, debug@npm:^4.4.0":
version: 4.4.0
resolution: "debug@npm:4.4.0"
dependencies:
@@ -7765,7 +7874,7 @@ __metadata:
languageName: node
linkType: hard
-"depd@npm:2.0.0":
+"depd@npm:2.0.0, depd@npm:^2.0.0":
version: 2.0.0
resolution: "depd@npm:2.0.0"
checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c
@@ -8036,6 +8145,13 @@ __metadata:
languageName: node
linkType: hard
+"encodeurl@npm:^2.0.0, encodeurl@npm:~2.0.0":
+ version: 2.0.0
+ resolution: "encodeurl@npm:2.0.0"
+ checksum: 10c0/5d317306acb13e6590e28e27924c754163946a2480de11865c991a3a7eed4315cd3fba378b543ca145829569eefe9b899f3d84bb09870f675ae60bc924b01ceb
+ languageName: node
+ linkType: hard
+
"encodeurl@npm:~1.0.2":
version: 1.0.2
resolution: "encodeurl@npm:1.0.2"
@@ -8043,13 +8159,6 @@ __metadata:
languageName: node
linkType: hard
-"encodeurl@npm:~2.0.0":
- version: 2.0.0
- resolution: "encodeurl@npm:2.0.0"
- checksum: 10c0/5d317306acb13e6590e28e27924c754163946a2480de11865c991a3a7eed4315cd3fba378b543ca145829569eefe9b899f3d84bb09870f675ae60bc924b01ceb
- languageName: node
- linkType: hard
-
"encoding@npm:^0.1.13":
version: 0.1.13
resolution: "encoding@npm:0.1.13"
@@ -8522,7 +8631,7 @@ __metadata:
languageName: node
linkType: hard
-"escape-html@npm:~1.0.3":
+"escape-html@npm:^1.0.3, escape-html@npm:~1.0.3":
version: 1.0.3
resolution: "escape-html@npm:1.0.3"
checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3
@@ -8851,7 +8960,7 @@ __metadata:
languageName: node
linkType: hard
-"etag@npm:~1.8.1":
+"etag@npm:^1.8.1, etag@npm:~1.8.1":
version: 1.8.1
resolution: "etag@npm:1.8.1"
checksum: 10c0/12be11ef62fb9817314d790089a0a49fae4e1b50594135dcb8076312b7d7e470884b5100d249b28c18581b7fd52f8b485689ffae22a11ed9ec17377a33a08f84
@@ -8873,6 +8982,22 @@ __metadata:
languageName: node
linkType: hard
+"eventsource-parser@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "eventsource-parser@npm:3.0.1"
+ checksum: 10c0/146ce5ae8325d07645a49bbc54d7ac3aef42f5138bfbbe83d5cf96293b50eab2219926d6cf41eed0a0f90132578089652ba9286a19297662900133a9da6c2fd0
+ languageName: node
+ linkType: hard
+
+"eventsource@npm:^3.0.2":
+ version: 3.0.6
+ resolution: "eventsource@npm:3.0.6"
+ dependencies:
+ eventsource-parser: "npm:^3.0.1"
+ checksum: 10c0/074d865ea1c7e29e3243f85a13306e89fca2d775b982dca03fa6bfa75c56827fa89cf1ab9e730db24bd6b104cbdcae074f2b37ba498874e9dd9710fbff4979bb
+ languageName: node
+ linkType: hard
+
"expect-type@npm:^1.1.0":
version: 1.2.0
resolution: "expect-type@npm:1.2.0"
@@ -8887,6 +9012,15 @@ __metadata:
languageName: node
linkType: hard
+"express-rate-limit@npm:^7.5.0":
+ version: 7.5.0
+ resolution: "express-rate-limit@npm:7.5.0"
+ peerDependencies:
+ express: ^4.11 || 5 || ^5.0.0-beta.1
+ checksum: 10c0/3e96afa05b4f577395688ede37e0cb19901f20c350b32575fb076f3d25176209fb88d3648151755c232aaf304147c58531f070757978f376e2f08326449299fd
+ languageName: node
+ linkType: hard
+
"express@npm:^4.21.2":
version: 4.21.2
resolution: "express@npm:4.21.2"
@@ -8926,6 +9060,41 @@ __metadata:
languageName: node
linkType: hard
+"express@npm:^5.0.1, 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
+
"fast-content-type-parse@npm:^2.0.0":
version: 2.0.1
resolution: "fast-content-type-parse@npm:2.0.1"
@@ -9056,6 +9225,20 @@ __metadata:
languageName: node
linkType: hard
+"finalhandler@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "finalhandler@npm:2.1.0"
+ dependencies:
+ debug: "npm:^4.4.0"
+ encodeurl: "npm:^2.0.0"
+ escape-html: "npm:^1.0.3"
+ on-finished: "npm:^2.4.1"
+ parseurl: "npm:^1.3.3"
+ statuses: "npm:^2.0.1"
+ checksum: 10c0/da0bbca6d03873472ee890564eb2183f4ed377f25f3628a0fc9d16dac40bed7b150a0d82ebb77356e4c6d97d2796ad2dba22948b951dddee2c8768b0d1b9fb1f
+ languageName: node
+ linkType: hard
+
"find-up@npm:^5.0.0":
version: 5.0.0
resolution: "find-up@npm:5.0.0"
@@ -9153,6 +9336,13 @@ __metadata:
languageName: node
linkType: hard
+"fresh@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "fresh@npm:2.0.0"
+ checksum: 10c0/0557548194cb9a809a435bf92bcfbc20c89e8b5eb38861b73ced36750437251e39a111fc3a18b98531be9dd91fe1411e4969f229dc579ec0251ce6c5d4900bbc
+ languageName: node
+ linkType: hard
+
"from@npm:~0":
version: 0.1.7
resolution: "from@npm:0.1.7"
@@ -9651,7 +9841,7 @@ __metadata:
languageName: node
linkType: hard
-"http-errors@npm:2.0.0":
+"http-errors@npm:2.0.0, http-errors@npm:^2.0.0":
version: 2.0.0
resolution: "http-errors@npm:2.0.0"
dependencies:
@@ -9710,7 +9900,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.2, iconv-lite@npm:^0.6.3":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
@@ -9726,6 +9916,13 @@ __metadata:
languageName: node
linkType: hard
+"ignore-by-default@npm:^1.0.1":
+ version: 1.0.1
+ resolution: "ignore-by-default@npm:1.0.1"
+ checksum: 10c0/9ab6e70e80f7cc12735def7ecb5527cfa56ab4e1152cd64d294522827f2dcf1f6d85531241537dc3713544e88dd888f65cb3c49c7b2cddb9009087c75274e533
+ languageName: node
+ linkType: hard
+
"ignore@npm:^5.2.0, ignore@npm:^5.3.1":
version: 5.3.2
resolution: "ignore@npm:5.3.2"
@@ -10035,6 +10232,13 @@ __metadata:
languageName: node
linkType: hard
+"is-promise@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "is-promise@npm:4.0.0"
+ checksum: 10c0/ebd5c672d73db781ab33ccb155fb9969d6028e37414d609b115cc534654c91ccd061821d5b987eefaa97cf4c62f0b909bb2f04db88306de26e91bfe8ddc01503
+ languageName: node
+ linkType: hard
+
"is-reference@npm:1.2.1":
version: 1.2.1
resolution: "is-reference@npm:1.2.1"
@@ -10783,6 +10987,13 @@ __metadata:
languageName: node
linkType: hard
+"media-typer@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "media-typer@npm:1.1.0"
+ checksum: 10c0/7b4baa40b25964bb90e2121ee489ec38642127e48d0cc2b6baa442688d3fde6262bfdca86d6bbf6ba708784afcac168c06840c71facac70e390f5f759ac121b9
+ languageName: node
+ linkType: hard
+
"memorystream@npm:^0.3.1":
version: 0.3.1
resolution: "memorystream@npm:0.3.1"
@@ -10797,6 +11008,13 @@ __metadata:
languageName: node
linkType: hard
+"merge-descriptors@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "merge-descriptors@npm:2.0.0"
+ checksum: 10c0/95389b7ced3f9b36fbdcf32eb946dc3dd1774c2fdf164609e55b18d03aa499b12bd3aae3a76c1c7185b96279e9803525550d3eb292b5224866060a288f335cb3
+ languageName: node
+ linkType: hard
+
"merge2@npm:^1.3.0, merge2@npm:^1.4.1":
version: 1.4.1
resolution: "merge2@npm:1.4.1"
@@ -10870,6 +11088,13 @@ __metadata:
languageName: node
linkType: hard
+"mime-db@npm:^1.54.0":
+ version: 1.54.0
+ resolution: "mime-db@npm:1.54.0"
+ checksum: 10c0/8d907917bc2a90fa2df842cdf5dfeaf509adc15fe0531e07bb2f6ab15992416479015828d6a74200041c492e42cce3ebf78e5ce714388a0a538ea9c53eece284
+ languageName: node
+ linkType: hard
+
"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
@@ -10879,6 +11104,15 @@ __metadata:
languageName: node
linkType: hard
+"mime-types@npm:^3.0.0, mime-types@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "mime-types@npm:3.0.1"
+ dependencies:
+ mime-db: "npm:^1.54.0"
+ checksum: 10c0/bd8c20d3694548089cf229016124f8f40e6a60bbb600161ae13e45f793a2d5bb40f96bbc61f275836696179c77c1d6bf4967b2a75e0a8ad40fe31f4ed5be4da5
+ languageName: node
+ linkType: hard
+
"mime@npm:1.6.0":
version: 1.6.0
resolution: "mime@npm:1.6.0"
@@ -11408,6 +11642,26 @@ __metadata:
languageName: node
linkType: hard
+"nodemon@npm:^3.1.10":
+ version: 3.1.10
+ resolution: "nodemon@npm:3.1.10"
+ dependencies:
+ chokidar: "npm:^3.5.2"
+ debug: "npm:^4"
+ ignore-by-default: "npm:^1.0.1"
+ minimatch: "npm:^3.1.2"
+ pstree.remy: "npm:^1.1.8"
+ semver: "npm:^7.5.3"
+ simple-update-notifier: "npm:^2.0.0"
+ supports-color: "npm:^5.5.0"
+ touch: "npm:^3.1.0"
+ undefsafe: "npm:^2.0.5"
+ bin:
+ nodemon: bin/nodemon.js
+ checksum: 10c0/95b64d647f2c22e85e375b250517b0a4b32c2d2392ad898444e331f70d6b1ab43b17f53a8a1d68d5879ab8401fc6cd6e26f0d2a8736240984f6b5a8435b407c0
+ languageName: node
+ linkType: hard
+
"nopt@npm:^8.0.0":
version: 8.1.0
resolution: "nopt@npm:8.1.0"
@@ -11576,7 +11830,7 @@ __metadata:
languageName: node
linkType: hard
-"on-finished@npm:2.4.1":
+"on-finished@npm:2.4.1, on-finished@npm:^2.4.1":
version: 2.4.1
resolution: "on-finished@npm:2.4.1"
dependencies:
@@ -11585,7 +11839,7 @@ __metadata:
languageName: node
linkType: hard
-"once@npm:^1.3.0":
+"once@npm:^1.3.0, once@npm:^1.4.0":
version: 1.4.0
resolution: "once@npm:1.4.0"
dependencies:
@@ -11751,7 +12005,7 @@ __metadata:
languageName: node
linkType: hard
-"parseurl@npm:~1.3.3":
+"parseurl@npm:^1.3.3, parseurl@npm:~1.3.3":
version: 1.3.3
resolution: "parseurl@npm:1.3.3"
checksum: 10c0/90dd4760d6f6174adb9f20cf0965ae12e23879b5f5464f38e92fce8073354341e4b3b76fa3d878351efe7d01e617121955284cfd002ab087fba1a0726ec0b4f5
@@ -11820,6 +12074,13 @@ __metadata:
languageName: node
linkType: hard
+"path-to-regexp@npm:^8.0.0":
+ version: 8.2.0
+ resolution: "path-to-regexp@npm:8.2.0"
+ checksum: 10c0/ef7d0a887b603c0a142fad16ccebdcdc42910f0b14830517c724466ad676107476bba2fe9fffd28fd4c141391ccd42ea426f32bb44c2c82ecaefe10c37b90f5a
+ languageName: node
+ linkType: hard
+
"path-type@npm:^3.0.0":
version: 3.0.0
resolution: "path-type@npm:3.0.0"
@@ -11973,6 +12234,13 @@ __metadata:
languageName: node
linkType: hard
+"pkce-challenge@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "pkce-challenge@npm:5.0.0"
+ checksum: 10c0/c6706d627fdbb6f22bf8cc5d60d96d6b6a7bb481399b336a3d3f4e9bfba3e167a2c32f8ec0b5e74be686a0ba3bcc9894865d4c2dd1b91cea4c05dba1f28602c3
+ languageName: node
+ linkType: hard
+
"possible-typed-array-names@npm:^1.0.0":
version: 1.1.0
resolution: "possible-typed-array-names@npm:1.1.0"
@@ -12328,7 +12596,7 @@ __metadata:
languageName: node
linkType: hard
-"proxy-addr@npm:~2.0.7":
+"proxy-addr@npm:^2.0.7, proxy-addr@npm:~2.0.7":
version: 2.0.7
resolution: "proxy-addr@npm:2.0.7"
dependencies:
@@ -12365,6 +12633,13 @@ __metadata:
languageName: node
linkType: hard
+"pstree.remy@npm:^1.1.8":
+ version: 1.1.8
+ resolution: "pstree.remy@npm:1.1.8"
+ checksum: 10c0/30f78c88ce6393cb3f7834216cb6e282eb83c92ccb227430d4590298ab2811bc4a4745f850a27c5178e79a8f3e316591de0fec87abc19da648c2b3c6eb766d14
+ languageName: node
+ linkType: hard
+
"punycode.js@npm:^2.3.1":
version: 2.3.1
resolution: "punycode.js@npm:2.3.1"
@@ -12388,7 +12663,7 @@ __metadata:
languageName: node
linkType: hard
-"qs@npm:^6.11.0, qs@npm:^6.12.2":
+"qs@npm:^6.11.0, qs@npm:^6.12.2, qs@npm:^6.14.0":
version: 6.14.0
resolution: "qs@npm:6.14.0"
dependencies:
@@ -12421,7 +12696,7 @@ __metadata:
languageName: node
linkType: hard
-"range-parser@npm:~1.2.1":
+"range-parser@npm:^1.2.1, range-parser@npm:~1.2.1":
version: 1.2.1
resolution: "range-parser@npm:1.2.1"
checksum: 10c0/96c032ac2475c8027b7a4e9fe22dc0dfe0f6d90b85e496e0f016fbdb99d6d066de0112e680805075bd989905e2123b3b3d002765149294dce0c1f7f01fcc2ea0
@@ -12447,6 +12722,18 @@ __metadata:
languageName: node
linkType: hard
+"raw-body@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "raw-body@npm:3.0.0"
+ dependencies:
+ bytes: "npm:3.1.2"
+ http-errors: "npm:2.0.0"
+ iconv-lite: "npm:0.6.3"
+ unpipe: "npm:1.0.0"
+ checksum: 10c0/f8daf4b724064a4811d118745a781ca0fb4676298b8adadfd6591155549cfea0a067523cf7dd3baeb1265fecc9ce5dfb2fc788c12c66b85202a336593ece0f87
+ languageName: node
+ linkType: hard
+
"react-device-detect@npm:^2.2.3":
version: 2.2.3
resolution: "react-device-detect@npm:2.2.3"
@@ -13082,6 +13369,19 @@ __metadata:
languageName: unknown
linkType: soft
+"router@npm:^2.2.0":
+ version: 2.2.0
+ resolution: "router@npm:2.2.0"
+ dependencies:
+ debug: "npm:^4.4.0"
+ depd: "npm:^2.0.0"
+ is-promise: "npm:^4.0.0"
+ parseurl: "npm:^1.3.3"
+ path-to-regexp: "npm:^8.0.0"
+ checksum: 10c0/3279de7450c8eae2f6e095e9edacbdeec0abb5cb7249c6e719faa0db2dba43574b4fff5892d9220631c9abaff52dd3cad648cfea2aaace845e1a071915ac8867
+ languageName: node
+ linkType: hard
+
"rrweb-cssom@npm:^0.7.1":
version: 0.7.1
resolution: "rrweb-cssom@npm:0.7.1"
@@ -13247,7 +13547,7 @@ __metadata:
languageName: node
linkType: hard
-"semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3":
+"semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3":
version: 7.7.1
resolution: "semver@npm:7.7.1"
bin:
@@ -13277,6 +13577,25 @@ __metadata:
languageName: node
linkType: hard
+"send@npm:^1.1.0, send@npm:^1.2.0":
+ version: 1.2.0
+ resolution: "send@npm:1.2.0"
+ dependencies:
+ debug: "npm:^4.3.5"
+ encodeurl: "npm:^2.0.0"
+ escape-html: "npm:^1.0.3"
+ etag: "npm:^1.8.1"
+ fresh: "npm:^2.0.0"
+ http-errors: "npm:^2.0.0"
+ mime-types: "npm:^3.0.1"
+ ms: "npm:^2.1.3"
+ on-finished: "npm:^2.4.1"
+ range-parser: "npm:^1.2.1"
+ statuses: "npm:^2.0.1"
+ checksum: 10c0/531bcfb5616948d3468d95a1fd0adaeb0c20818ba4a500f439b800ca2117971489e02074ce32796fd64a6772ea3e7235fe0583d8241dbd37a053dc3378eff9a5
+ languageName: node
+ linkType: hard
+
"serialize-error@npm:8.1.0":
version: 8.1.0
resolution: "serialize-error@npm:8.1.0"
@@ -13298,6 +13617,18 @@ __metadata:
languageName: node
linkType: hard
+"serve-static@npm:^2.2.0":
+ version: 2.2.0
+ resolution: "serve-static@npm:2.2.0"
+ dependencies:
+ encodeurl: "npm:^2.0.0"
+ escape-html: "npm:^1.0.3"
+ parseurl: "npm:^1.3.3"
+ send: "npm:^1.2.0"
+ checksum: 10c0/30e2ed1dbff1984836cfd0c65abf5d3f3f83bcd696c99d2d3c97edbd4e2a3ff4d3f87108a7d713640d290a7b6fe6c15ddcbc61165ab2eaad48ea8d3b52c7f913
+ languageName: node
+ linkType: hard
+
"server-only@npm:^0.0.1":
version: 0.0.1
resolution: "server-only@npm:0.0.1"
@@ -13569,6 +13900,15 @@ __metadata:
languageName: node
linkType: hard
+"simple-update-notifier@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "simple-update-notifier@npm:2.0.0"
+ dependencies:
+ semver: "npm:^7.5.3"
+ checksum: 10c0/2a00bd03bfbcbf8a737c47ab230d7920f8bfb92d1159d421bdd194479f6d01ebc995d13fbe13d45dace23066a78a3dc6642999b4e3b38b847e6664191575b20c
+ languageName: node
+ linkType: hard
+
"slash@npm:^3.0.0":
version: 3.0.0
resolution: "slash@npm:3.0.0"
@@ -13758,7 +14098,7 @@ __metadata:
languageName: node
linkType: hard
-"statuses@npm:2.0.1":
+"statuses@npm:2.0.1, statuses@npm:^2.0.1":
version: 2.0.1
resolution: "statuses@npm:2.0.1"
checksum: 10c0/34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0
@@ -14025,7 +14365,7 @@ __metadata:
languageName: node
linkType: hard
-"supports-color@npm:^5.3.0":
+"supports-color@npm:^5.3.0, supports-color@npm:^5.5.0":
version: 5.5.0
resolution: "supports-color@npm:5.5.0"
dependencies:
@@ -14254,6 +14594,15 @@ __metadata:
languageName: node
linkType: hard
+"touch@npm:^3.1.0":
+ version: 3.1.1
+ resolution: "touch@npm:3.1.1"
+ bin:
+ nodetouch: bin/nodetouch.js
+ checksum: 10c0/d2e4d269a42c846a22a29065b9af0b263de58effc85a1764bb7a2e8fc4b47700e9e2fcbd7eb1f5bffbb7c73d860f93600cef282b93ddac8f0b62321cb498b36e
+ languageName: node
+ linkType: hard
+
"tough-cookie@npm:^5.0.0":
version: 5.1.2
resolution: "tough-cookie@npm:5.1.2"
@@ -14318,6 +14667,22 @@ __metadata:
languageName: node
linkType: hard
+"tsc-watch@npm:6.2.1":
+ version: 6.2.1
+ resolution: "tsc-watch@npm:6.2.1"
+ dependencies:
+ cross-spawn: "npm:^7.0.3"
+ node-cleanup: "npm:^2.1.2"
+ ps-tree: "npm:^1.2.0"
+ string-argv: "npm:^0.3.1"
+ peerDependencies:
+ typescript: "*"
+ bin:
+ tsc-watch: dist/lib/tsc-watch.js
+ checksum: 10c0/f5fe19e5ac9f4c42a5600c20aee9ff49e282f11813aead65ed58fa11d98a20f5a82bf4f931897270f49f6475dd54e9aab9c46a07c3801b8d237dfbe77bcf1bfc
+ languageName: node
+ linkType: hard
+
"tsc-watch@npm:^6.2.0":
version: 6.3.0
resolution: "tsc-watch@npm:6.3.0"
@@ -14367,7 +14732,7 @@ __metadata:
languageName: node
linkType: hard
-"tsx@npm:^4.19.1, tsx@npm:^4.19.2":
+"tsx@npm:^4.0.0, tsx@npm:^4.19.1, tsx@npm:^4.19.2":
version: 4.19.3
resolution: "tsx@npm:4.19.3"
dependencies:
@@ -14406,6 +14771,17 @@ __metadata:
languageName: node
linkType: hard
+"type-is@npm:^2.0.0, type-is@npm:^2.0.1":
+ version: 2.0.1
+ resolution: "type-is@npm:2.0.1"
+ dependencies:
+ content-type: "npm:^1.0.5"
+ media-typer: "npm:^1.1.0"
+ mime-types: "npm:^3.0.0"
+ checksum: 10c0/7f7ec0a060b16880bdad36824ab37c26019454b67d73e8a465ed5a3587440fbe158bc765f0da68344498235c877e7dbbb1600beccc94628ed05599d667951b99
+ languageName: node
+ linkType: hard
+
"type-is@npm:~1.6.18":
version: 1.6.18
resolution: "type-is@npm:1.6.18"
@@ -14479,6 +14855,16 @@ __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"
@@ -14489,6 +14875,16 @@ __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"
@@ -14517,6 +14913,13 @@ __metadata:
languageName: node
linkType: hard
+"undefsafe@npm:^2.0.5":
+ version: 2.0.5
+ resolution: "undefsafe@npm:2.0.5"
+ checksum: 10c0/96c0466a5fbf395917974a921d5d4eee67bca4b30d3a31ce7e621e0228c479cf893e783a109af6e14329b52fe2f0cb4108665fad2b87b0018c0df6ac771261d5
+ languageName: node
+ linkType: hard
+
"undici-types@npm:~6.19.2":
version: 6.19.8
resolution: "undici-types@npm:6.19.8"
@@ -14728,7 +15131,7 @@ __metadata:
languageName: node
linkType: hard
-"vary@npm:^1, vary@npm:~1.1.2":
+"vary@npm:^1, vary@npm:^1.1.2, vary@npm:~1.1.2":
version: 1.1.2
resolution: "vary@npm:1.1.2"
checksum: 10c0/f15d588d79f3675135ba783c91a4083dcd290a2a5be9fcb6514220a1634e23df116847b1cc51f66bfb0644cf9353b2abb7815ae499bab06e46dd33c1a6bf1f4f
@@ -15249,10 +15652,19 @@ __metadata:
languageName: node
linkType: hard
-"zod@npm:^3.24.2":
- version: 3.24.2
- resolution: "zod@npm:3.24.2"
- checksum: 10c0/c638c7220150847f13ad90635b3e7d0321b36cce36f3fc6050ed960689594c949c326dfe2c6fa87c14b126ee5d370ccdebd6efb304f41ef5557a4aaca2824565
+"zod-to-json-schema@npm:^3.24.1":
+ version: 3.24.5
+ resolution: "zod-to-json-schema@npm:3.24.5"
+ peerDependencies:
+ zod: ^3.24.1
+ checksum: 10c0/0745b94ba53e652d39f262641cdeb2f75d24339fb6076a38ce55bcf53d82dfaea63adf524ebc5f658681005401687f8e9551c4feca7c4c882e123e66091dfb90
+ languageName: node
+ linkType: hard
+
+"zod@npm:^3.23.8, zod@npm:^3.24.3":
+ version: 3.24.3
+ resolution: "zod@npm:3.24.3"
+ checksum: 10c0/ab0369810968d0329a1a141e9418e01e5c9c2a4905cbb7cb7f5a131d6e9487596e1400e21eeff24c4a8ee28dacfa5bd6103893765c055b7a98c2006a5a4fc68d
languageName: node
linkType: hard