Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added ask sidebar to homepage. [#721](https://github.com/sourcebot-dev/sourcebot/pull/721)
- Added endpoint for searching commit history for a git repository. [#625](https://github.com/sourcebot-dev/sourcebot/pull/625)
- Added `pushedAt` field to the Repo table to track when a repository last was committed to across all branches. [#790](https://github.com/sourcebot-dev/sourcebot/pull/790)
- Added offset pagination to the `/api/repos` endpoint. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
- Added offset pagination to the `/api/commits` endpoint. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)

### Changed
- Added commit graph generation to improve performance for commit traversal operations. [#791](https://github.com/sourcebot-dev/sourcebot/pull/791)
- Made the code search `lang:` filter case insensitive. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
- Changed the `/api/source` endpoint from a POST request to a GET request. Repo, path, and ref are specified as query params. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
- Changed the `/api/commits` endpoint from a POST request to a GET request. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
- Renamed `webUrl` to `externalWebUrl` for various apis. Moving forward, `webUrl` will be used for URLs that point to Sourcebot, and `externalWebUrl` will be used for URLs that point to external code hosts. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
- Renamed various fields on the `/api/source` endpoint response body. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)

### Fixed
- Fixed issue where a file would fail to load when opening it from the /search view and it matched multiple branches. [#797](https://github.com/sourcebot-dev/sourcebot/pull/797)
Expand Down
7 changes: 7 additions & 0 deletions packages/mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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

Expand Down
1 change: 1 addition & 0 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"dependencies": {
"@modelcontextprotocol/sdk": "^1.10.2",
"@t3-oss/env-core": "^0.13.4",
"dedent": "^1.7.1",
"escape-string-regexp": "^5.0.0",
"express": "^5.1.0",
"zod": "^3.24.3"
Expand Down
115 changes: 74 additions & 41 deletions packages/mcp/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,105 @@
import { env } from './env.js';
import { listRepositoriesResponseSchema, searchResponseSchema, fileSourceResponseSchema, searchCommitsResponseSchema } from './schemas.js';
import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse, ServiceError, SearchCommitsRequest, SearchCommitsResponse } from './types.js';
import { isServiceError } from './utils.js';
import { listReposResponseSchema, searchResponseSchema, fileSourceResponseSchema, listCommitsResponseSchema } from './schemas.js';
import { FileSourceRequest, ListReposQueryParams, SearchRequest, ListCommitsQueryParamsSchema } from './types.js';
import { isServiceError, ServiceErrorException } from './utils.js';
import { z } from 'zod';

export const search = async (request: SearchRequest): Promise<SearchResponse | ServiceError> => {
const result = await fetch(`${env.SOURCEBOT_HOST}/api/search`, {
const parseResponse = async <T extends z.ZodTypeAny>(
response: Response,
schema: T
): Promise<z.infer<T>> => {
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',
...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
},
body: JSON.stringify(request)
}).then(response => response.json());
});

if (isServiceError(result)) {
return result;
}

return searchResponseSchema.parse(result);
return parseResponse(response, searchResponseSchema);
}

export const listRepos = async (): Promise<ListRepositoriesResponse | ServiceError> => {
const result = await fetch(`${env.SOURCEBOT_HOST}/api/repos`, {
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',
...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
},
}).then(response => response.json());

if (isServiceError(result)) {
return result;
}
});

return listRepositoriesResponseSchema.parse(result);
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): Promise<FileSourceResponse | ServiceError> => {
const result = await fetch(`${env.SOURCEBOT_HOST}/api/source`, {
method: 'POST',
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: {
'Content-Type': 'application/json',
...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
},
body: JSON.stringify(request)
}).then(response => response.json());

if (isServiceError(result)) {
return result;
}
});

return fileSourceResponseSchema.parse(result);
return parseResponse(response, fileSourceResponseSchema);
}

export const searchCommits = async (request: SearchCommitsRequest): Promise<SearchCommitsResponse | ServiceError> => {
const result = await fetch(`${env.SOURCEBOT_HOST}/api/commits`, {
method: 'POST',
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: {
'Content-Type': 'application/json',
'X-Org-Domain': '~',
...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
},
body: JSON.stringify(request)
}).then(response => response.json());

if (isServiceError(result)) {
return result;
}
});

return searchCommitsResponseSchema.parse(result);
const commits = await parseResponse(response, listCommitsResponseSchema);
const totalCount = parseInt(response.headers.get('X-Total-Count') ?? '0', 10);
return { commits, totalCount };
}
Loading