@sourcebot/mcp 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.14] - 2026-01-27
11
+
12
+ ### Changed
13
+ - Updated README.
14
+
15
+ ## [1.0.13] - 2026-01-27
16
+
17
+ ### Added
18
+ - Added `search_commits` tool to search a repos commit history. [#625](https://github.com/sourcebot-dev/sourcebot/pull/625)
19
+ - Added `gitRevision` parameter to the `search_code` tool to allow for searching on different branches. [#625](https://github.com/sourcebot-dev/sourcebot/pull/625)
20
+ - Added server side pagination support for `list_commits` and `list_repos`. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
21
+ - Added `filterByFilepaths` and `useRegex` params to the `search_code` tool. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
22
+
23
+ ### Changed
24
+ - Renamed `search_commits` tool to `list_commits`. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
25
+ - Renamed `gitRevision` param to `ref` on `search_code` tool. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
26
+ - Generally improved tool and tool param descriptions for all tools. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
27
+
10
28
  ## [1.0.12] - 2026-01-13
11
29
 
12
30
  ### Fixed
package/README.md CHANGED
@@ -164,19 +164,22 @@ For a more detailed guide, checkout [the docs](https://docs.sourcebot.dev/docs/f
164
164
 
165
165
  ### search_code
166
166
 
167
- Fetches code that matches the provided regex pattern in `query`.
167
+ Searches for code that matches the provided search query as a substring by default, or as a regular expression if `useRegex` is true.
168
168
 
169
169
  <details>
170
170
  <summary>Parameters</summary>
171
171
 
172
172
  | Name | Required | Description |
173
173
  |:----------------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------|
174
- | `query` | yes | Regex pattern to search for. Escape special characters and spaces with a single backslash (e.g., 'console\.log', 'console\ log'). |
175
- | `filterByRepoIds` | no | Restrict search to specific repository IDs (from 'list_repos'). Leave empty to search all. |
176
- | `filterByLanguages` | no | Restrict search to specific languages (GitHub linguist format, e.g., Python, JavaScript). |
177
- | `caseSensitive` | no | Case sensitive search (default: false). |
178
- | `includeCodeSnippets` | no | Include code snippets in results (default: false). |
179
- | `maxTokens` | no | Max tokens to return (default: env.DEFAULT_MINIMUM_TOKENS). |
174
+ | `query` | yes | The search pattern to match against code contents. Do not escape quotes in your query. |
175
+ | `useRegex` | no | Whether to use regular expression matching. When false, substring matching is used (default: false). |
176
+ | `filterByRepos` | no | Scope the search to specific repositories. |
177
+ | `filterByLanguages` | no | Scope the search to specific languages. |
178
+ | `filterByFilepaths` | no | Scope the search to specific filepaths. |
179
+ | `caseSensitive` | no | Whether the search should be case sensitive (default: false). |
180
+ | `includeCodeSnippets` | no | Whether to include code snippets in the response (default: false). |
181
+ | `ref` | no | Commit SHA, branch or tag name to search on. If not provided, defaults to the default branch. |
182
+ | `maxTokens` | no | The maximum number of tokens to return (default: 10000). Higher values provide more context but consume more tokens. |
180
183
  </details>
181
184
 
182
185
 
@@ -187,25 +190,48 @@ Lists repositories indexed by Sourcebot with optional filtering and pagination.
187
190
  <details>
188
191
  <summary>Parameters</summary>
189
192
 
190
- | Name | Required | Description |
191
- |:-------------|:---------|:--------------------------------------------------------------------|
192
- | `query` | no | Filter repositories by name (case-insensitive). |
193
- | `pageNumber` | no | Page number (1-indexed, default: 1). |
194
- | `limit` | no | Number of repositories per page (default: 50). |
193
+ | Name | Required | Description |
194
+ |:------------|:---------|:--------------------------------------------------------------------------------|
195
+ | `query` | no | Filter repositories by name (case-insensitive). |
196
+ | `page` | no | Page number for pagination (min 1, default: 1). |
197
+ | `perPage` | no | Results per page for pagination (min 1, max 100, default: 30). |
198
+ | `sort` | no | Sort repositories by 'name' or 'pushed' (most recent commit). Default: 'name'. |
199
+ | `direction` | no | Sort direction: 'asc' or 'desc' (default: 'asc'). |
195
200
 
196
201
  </details>
197
202
 
198
- ### get_file_source
203
+ ### read_file
199
204
 
200
- Fetches the source code for a given file.
205
+ Reads the source code for a given file.
201
206
 
202
207
  <details>
203
208
  <summary>Parameters</summary>
204
209
 
205
- | Name | Required | Description |
206
- |:-------------|:---------|:-----------------------------------------------------------------|
207
- | `fileName` | yes | The file to fetch the source code for. |
208
- | `repoId` | yes | The Sourcebot repository ID. |
210
+ | Name | Required | Description |
211
+ |:-------|:---------|:---------------------------------------------------------------------------------------------------------------|
212
+ | `repo` | yes | The repository name. |
213
+ | `path` | yes | The path to the file. |
214
+ | `ref` | no | Commit SHA, branch or tag name to fetch the source code for. If not provided, uses the default branch. |
215
+ </details>
216
+
217
+ ### list_commits
218
+
219
+ Get a list of commits for a given repository.
220
+
221
+ <details>
222
+ <summary>Parameters</summary>
223
+
224
+ | Name | Required | Description |
225
+ |:----------|:---------|:--------------------------------------------------------------------------------------------------------------------------------------|
226
+ | `repo` | yes | The name of the repository to list commits for. |
227
+ | `query` | no | Search query to filter commits by message content (case-insensitive). |
228
+ | `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'). |
229
+ | `until` | no | Show commits older than this date. Supports ISO 8601 (e.g., '2024-12-31') or relative formats (e.g., 'yesterday'). |
230
+ | `author` | no | Filter commits by author name or email (case-insensitive). |
231
+ | `ref` | no | Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch. |
232
+ | `page` | no | Page number for pagination (min 1, default: 1). |
233
+ | `perPage` | no | Results per page for pagination (min 1, max 100, default: 50). |
234
+
209
235
  </details>
210
236
 
211
237
 
package/dist/client.d.ts CHANGED
@@ -1,4 +1,124 @@
1
- import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse, ServiceError } from './types.js';
2
- export declare const search: (request: SearchRequest) => Promise<SearchResponse | ServiceError>;
3
- export declare const listRepos: () => Promise<ListRepositoriesResponse | ServiceError>;
4
- export declare const getFileSource: (request: FileSourceRequest) => Promise<FileSourceResponse | ServiceError>;
1
+ import { FileSourceRequest, ListReposQueryParams, SearchRequest, ListCommitsQueryParamsSchema } from './types.js';
2
+ export declare const search: (request: SearchRequest) => Promise<{
3
+ stats: {
4
+ actualMatchCount: number;
5
+ totalMatchCount: number;
6
+ duration: number;
7
+ fileCount: number;
8
+ filesSkipped: number;
9
+ contentBytesLoaded: number;
10
+ indexBytesLoaded: number;
11
+ crashes: number;
12
+ shardFilesConsidered: number;
13
+ filesConsidered: number;
14
+ filesLoaded: number;
15
+ shardsScanned: number;
16
+ shardsSkipped: number;
17
+ shardsSkippedFilter: number;
18
+ ngramMatches: number;
19
+ ngramLookups: number;
20
+ wait: number;
21
+ matchTreeConstruction: number;
22
+ matchTreeSearch: number;
23
+ regexpsConsidered: number;
24
+ flushReason: string;
25
+ };
26
+ files: {
27
+ webUrl: string;
28
+ fileName: {
29
+ text: string;
30
+ matchRanges: {
31
+ start: {
32
+ byteOffset: number;
33
+ lineNumber: number;
34
+ column: number;
35
+ };
36
+ end: {
37
+ byteOffset: number;
38
+ lineNumber: number;
39
+ column: number;
40
+ };
41
+ }[];
42
+ };
43
+ repository: string;
44
+ repositoryId: number;
45
+ language: string;
46
+ chunks: {
47
+ matchRanges: {
48
+ start: {
49
+ byteOffset: number;
50
+ lineNumber: number;
51
+ column: number;
52
+ };
53
+ end: {
54
+ byteOffset: number;
55
+ lineNumber: number;
56
+ column: number;
57
+ };
58
+ }[];
59
+ content: string;
60
+ contentStart: {
61
+ byteOffset: number;
62
+ lineNumber: number;
63
+ column: number;
64
+ };
65
+ symbols?: {
66
+ symbol: string;
67
+ kind: string;
68
+ parent?: {
69
+ symbol: string;
70
+ kind: string;
71
+ } | undefined;
72
+ }[] | undefined;
73
+ }[];
74
+ externalWebUrl?: string | undefined;
75
+ content?: string | undefined;
76
+ branches?: string[] | undefined;
77
+ }[];
78
+ repositoryInfo: {
79
+ id: number;
80
+ codeHostType: string;
81
+ name: string;
82
+ displayName?: string | undefined;
83
+ webUrl?: string | undefined;
84
+ }[];
85
+ isSearchExhaustive: boolean;
86
+ }>;
87
+ export declare const listRepos: (queryParams?: ListReposQueryParams) => Promise<{
88
+ repos: {
89
+ codeHostType: string;
90
+ webUrl: string;
91
+ repoId: number;
92
+ repoName: string;
93
+ externalWebUrl?: string | undefined;
94
+ repoDisplayName?: string | undefined;
95
+ imageUrl?: string | undefined;
96
+ indexedAt?: Date | undefined;
97
+ pushedAt?: Date | undefined;
98
+ }[];
99
+ totalCount: number;
100
+ }>;
101
+ export declare const getFileSource: (request: FileSourceRequest) => Promise<{
102
+ path: string;
103
+ source: string;
104
+ webUrl: string;
105
+ language: string;
106
+ repo: string;
107
+ repoCodeHostType: string;
108
+ externalWebUrl?: string | undefined;
109
+ repoDisplayName?: string | undefined;
110
+ repoExternalWebUrl?: string | undefined;
111
+ branch?: string | undefined;
112
+ }>;
113
+ export declare const listCommits: (queryParams: ListCommitsQueryParamsSchema) => Promise<{
114
+ commits: {
115
+ message: string;
116
+ date: string;
117
+ hash: string;
118
+ refs: string;
119
+ body: string;
120
+ author_name: string;
121
+ author_email: string;
122
+ }[];
123
+ totalCount: number;
124
+ }>;
package/dist/client.js CHANGED
@@ -1,45 +1,85 @@
1
1
  import { env } from './env.js';
2
- import { listRepositoriesResponseSchema, searchResponseSchema, fileSourceResponseSchema } from './schemas.js';
3
- import { isServiceError } from './utils.js';
2
+ import { listReposResponseSchema, searchResponseSchema, fileSourceResponseSchema, listCommitsResponseSchema } from './schemas.js';
3
+ import { isServiceError, ServiceErrorException } from './utils.js';
4
+ const parseResponse = async (response, schema) => {
5
+ const text = await response.text();
6
+ let json;
7
+ try {
8
+ json = JSON.parse(text);
9
+ }
10
+ catch {
11
+ throw new Error(`Invalid JSON response: ${text}`);
12
+ }
13
+ // Check if the response is already a service error from the API
14
+ if (isServiceError(json)) {
15
+ throw new ServiceErrorException(json);
16
+ }
17
+ const parsed = schema.safeParse(json);
18
+ if (!parsed.success) {
19
+ throw new Error(`Failed to parse response: ${parsed.error.message}`);
20
+ }
21
+ return parsed.data;
22
+ };
4
23
  export const search = async (request) => {
5
- const result = await fetch(`${env.SOURCEBOT_HOST}/api/search`, {
24
+ const response = await fetch(`${env.SOURCEBOT_HOST}/api/search`, {
6
25
  method: 'POST',
7
26
  headers: {
8
27
  'Content-Type': 'application/json',
9
28
  ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
10
29
  },
11
30
  body: JSON.stringify(request)
12
- }).then(response => response.json());
13
- if (isServiceError(result)) {
14
- return result;
15
- }
16
- return searchResponseSchema.parse(result);
31
+ });
32
+ return parseResponse(response, searchResponseSchema);
17
33
  };
18
- export const listRepos = async () => {
19
- const result = await fetch(`${env.SOURCEBOT_HOST}/api/repos`, {
34
+ export const listRepos = async (queryParams = {}) => {
35
+ const url = new URL(`${env.SOURCEBOT_HOST}/api/repos`);
36
+ for (const [key, value] of Object.entries(queryParams)) {
37
+ if (value) {
38
+ url.searchParams.set(key, value.toString());
39
+ }
40
+ }
41
+ const response = await fetch(url, {
20
42
  method: 'GET',
21
43
  headers: {
22
44
  'Content-Type': 'application/json',
23
45
  ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
24
46
  },
25
- }).then(response => response.json());
26
- if (isServiceError(result)) {
27
- return result;
28
- }
29
- return listRepositoriesResponseSchema.parse(result);
47
+ });
48
+ const repos = await parseResponse(response, listReposResponseSchema);
49
+ const totalCount = parseInt(response.headers.get('X-Total-Count') ?? '0', 10);
50
+ return { repos, totalCount };
30
51
  };
31
52
  export const getFileSource = async (request) => {
32
- const result = await fetch(`${env.SOURCEBOT_HOST}/api/source`, {
33
- method: 'POST',
53
+ const url = new URL(`${env.SOURCEBOT_HOST}/api/source`);
54
+ for (const [key, value] of Object.entries(request)) {
55
+ if (value) {
56
+ url.searchParams.set(key, value.toString());
57
+ }
58
+ }
59
+ const response = await fetch(url, {
60
+ method: 'GET',
34
61
  headers: {
35
- 'Content-Type': 'application/json',
36
62
  ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
37
63
  },
38
- body: JSON.stringify(request)
39
- }).then(response => response.json());
40
- if (isServiceError(result)) {
41
- return result;
64
+ });
65
+ return parseResponse(response, fileSourceResponseSchema);
66
+ };
67
+ export const listCommits = async (queryParams) => {
68
+ const url = new URL(`${env.SOURCEBOT_HOST}/api/commits`);
69
+ for (const [key, value] of Object.entries(queryParams)) {
70
+ if (value) {
71
+ url.searchParams.set(key, value.toString());
72
+ }
42
73
  }
43
- return fileSourceResponseSchema.parse(result);
74
+ const response = await fetch(url, {
75
+ method: 'GET',
76
+ headers: {
77
+ 'X-Org-Domain': '~',
78
+ ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
79
+ },
80
+ });
81
+ const commits = await parseResponse(response, listCommitsResponseSchema);
82
+ const totalCount = parseInt(response.headers.get('X-Total-Count') ?? '0', 10);
83
+ return { commits, totalCount };
44
84
  };
45
85
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,8BAA8B,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAE9G,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,CAAC,MAAM,MAAM,GAAG,KAAK,EAAE,OAAsB,EAA0C,EAAE;IAC3F,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,GAAG,CAAC,cAAc,aAAa,EAAE;QAC3D,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACL,cAAc,EAAE,kBAAkB;YAClC,GAAG,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,qBAAqB,EAAE,GAAG,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrF;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;KAChC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAErC,IAAI,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,OAAO,oBAAoB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AAC9C,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,IAAsD,EAAE;IAClF,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,GAAG,CAAC,cAAc,YAAY,EAAE;QAC1D,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACL,cAAc,EAAE,kBAAkB;YAClC,GAAG,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,qBAAqB,EAAE,GAAG,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrF;KACJ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAErC,IAAI,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,OAAO,8BAA8B,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AACxD,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,OAA0B,EAA8C,EAAE;IAC1G,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,GAAG,CAAC,cAAc,aAAa,EAAE;QAC3D,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACL,cAAc,EAAE,kBAAkB;YAClC,GAAG,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,qBAAqB,EAAE,GAAG,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrF;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;KAChC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAErC,IAAI,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,OAAO,wBAAwB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AAClD,CAAC,CAAA","sourcesContent":["import { env } from './env.js';\nimport { listRepositoriesResponseSchema, searchResponseSchema, fileSourceResponseSchema } from './schemas.js';\nimport { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse, ServiceError } from './types.js';\nimport { isServiceError } from './utils.js';\n\nexport const search = async (request: SearchRequest): Promise<SearchResponse | ServiceError> => {\n const result = await fetch(`${env.SOURCEBOT_HOST}/api/search`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})\n },\n body: JSON.stringify(request)\n }).then(response => response.json());\n\n if (isServiceError(result)) {\n return result;\n }\n\n return searchResponseSchema.parse(result);\n}\n\nexport const listRepos = async (): Promise<ListRepositoriesResponse | ServiceError> => {\n const result = await fetch(`${env.SOURCEBOT_HOST}/api/repos`, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})\n },\n }).then(response => response.json());\n\n if (isServiceError(result)) {\n return result;\n }\n\n return listRepositoriesResponseSchema.parse(result);\n}\n\nexport const getFileSource = async (request: FileSourceRequest): Promise<FileSourceResponse | ServiceError> => {\n const result = await fetch(`${env.SOURCEBOT_HOST}/api/source`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})\n },\n body: JSON.stringify(request)\n }).then(response => response.json());\n\n if (isServiceError(result)) {\n return result;\n }\n\n return fileSourceResponseSchema.parse(result);\n}\n"]}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { env } from './env.js';\nimport { listReposResponseSchema, searchResponseSchema, fileSourceResponseSchema, listCommitsResponseSchema } from './schemas.js';\nimport { FileSourceRequest, ListReposQueryParams, SearchRequest, ListCommitsQueryParamsSchema } from './types.js';\nimport { isServiceError, ServiceErrorException } from './utils.js';\nimport { z } from 'zod';\n\nconst parseResponse = async <T extends z.ZodTypeAny>(\n response: Response,\n schema: T\n): Promise<z.infer<T>> => {\n const text = await response.text();\n\n let json: unknown;\n try {\n json = JSON.parse(text);\n } catch {\n throw new Error(`Invalid JSON response: ${text}`);\n }\n\n // Check if the response is already a service error from the API\n if (isServiceError(json)) {\n throw new ServiceErrorException(json);\n }\n\n const parsed = schema.safeParse(json);\n if (!parsed.success) {\n throw new Error(`Failed to parse response: ${parsed.error.message}`);\n }\n\n return parsed.data;\n};\n\nexport const search = async (request: SearchRequest) => {\n const response = await fetch(`${env.SOURCEBOT_HOST}/api/search`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})\n },\n body: JSON.stringify(request)\n });\n\n return parseResponse(response, searchResponseSchema);\n}\n\nexport const listRepos = async (queryParams: ListReposQueryParams = {}) => {\n const url = new URL(`${env.SOURCEBOT_HOST}/api/repos`);\n\n for (const [key, value] of Object.entries(queryParams)) {\n if (value) {\n url.searchParams.set(key, value.toString());\n }\n }\n\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})\n },\n });\n\n const repos = await parseResponse(response, listReposResponseSchema);\n const totalCount = parseInt(response.headers.get('X-Total-Count') ?? '0', 10);\n return { repos, totalCount };\n}\n\nexport const getFileSource = async (request: FileSourceRequest) => {\n const url = new URL(`${env.SOURCEBOT_HOST}/api/source`);\n for (const [key, value] of Object.entries(request)) {\n if (value) {\n url.searchParams.set(key, value.toString());\n }\n }\n\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})\n },\n });\n\n return parseResponse(response, fileSourceResponseSchema);\n}\n\nexport const listCommits = async (queryParams: ListCommitsQueryParamsSchema) => {\n const url = new URL(`${env.SOURCEBOT_HOST}/api/commits`);\n for (const [key, value] of Object.entries(queryParams)) {\n if (value) {\n url.searchParams.set(key, value.toString());\n }\n }\n\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n 'X-Org-Domain': '~',\n ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})\n },\n });\n\n const commits = await parseResponse(response, listCommitsResponseSchema);\n const totalCount = parseInt(response.headers.get('X-Total-Count') ?? '0', 10);\n return { commits, totalCount };\n}\n"]}
package/dist/index.js CHANGED
@@ -2,36 +2,45 @@
2
2
  // Entry point for the MCP server
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import _dedent from "dedent";
5
6
  import escapeStringRegexp from 'escape-string-regexp';
6
7
  import { z } from 'zod';
7
- import { listRepos, search, getFileSource } from './client.js';
8
+ import { getFileSource, listCommits, listRepos, search } from './client.js';
8
9
  import { env, numberSchema } from './env.js';
9
- import { listReposRequestSchema } from './schemas.js';
10
- import { isServiceError } from './utils.js';
10
+ import { fileSourceRequestSchema, listCommitsQueryParamsSchema, listReposQueryParamsSchema } from './schemas.js';
11
+ const dedent = _dedent.withOptions({ alignValues: true });
11
12
  // Create MCP server
12
13
  const server = new McpServer({
13
14
  name: 'sourcebot-mcp-server',
14
15
  version: '0.1.0',
15
16
  });
16
- server.tool("search_code", `Fetches code that matches the provided regex pattern in \`query\`. This is NOT a semantic search.
17
- Results are returned as an array of matching files, with the file's URL, repository, and language.
18
- If you receive an error that indicates that you're not authenticated, please inform the user to set the SOURCEBOT_API_KEY environment variable.
19
- 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).
20
- 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.
21
- **ONLY USE** the \`filterByRepoIds\` property if the request requires searching a specific repo(s). Otherwise, leave it empty.`, {
17
+ server.tool("search_code", dedent `
18
+ 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.
19
+ `, {
22
20
  query: z
23
21
  .string()
24
- .describe(`The regex pattern to search for. RULES:
25
- 1. When a regex special character needs to be escaped, ALWAYS use a single backslash (\) (e.g., 'console\.log')
26
- 2. **ALWAYS** escape spaces with a single backslash (\) (e.g., 'console\ log')
27
- `),
28
- filterByRepoIds: z
22
+ .describe(`The search pattern to match against code contents. Do not escape quotes in your query.`)
23
+ // Escape backslashes first, then quotes, and wrap in double quotes
24
+ // so the query is treated as a literal phrase (like grep).
25
+ .transform((val) => {
26
+ const escaped = val.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
27
+ return `"${escaped}"`;
28
+ }),
29
+ useRegex: z
30
+ .boolean()
31
+ .describe(`Whether to use regular expression matching to match the search query against code contents. When false, substring matching is used. (default: false)`)
32
+ .optional(),
33
+ filterByRepos: z
29
34
  .array(z.string())
30
- .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.`)
35
+ .describe(`Scope the search to the provided repositories.`)
31
36
  .optional(),
32
37
  filterByLanguages: z
33
38
  .array(z.string())
34
- .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`)
39
+ .describe(`Scope the search to the provided languages.`)
40
+ .optional(),
41
+ filterByFilepaths: z
42
+ .array(z.string())
43
+ .describe(`Scope the search to the provided filepaths.`)
35
44
  .optional(),
36
45
  caseSensitive: z
37
46
  .boolean()
@@ -39,35 +48,37 @@ server.tool("search_code", `Fetches code that matches the provided regex pattern
39
48
  .optional(),
40
49
  includeCodeSnippets: z
41
50
  .boolean()
42
- .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.`)
51
+ .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)`)
52
+ .optional(),
53
+ ref: z
54
+ .string()
55
+ .describe(`Commit SHA, branch or tag name to search on. If not provided, defaults to the default branch (usually 'main' or 'master').`)
43
56
  .optional(),
44
57
  maxTokens: numberSchema
45
58
  .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.`)
46
59
  .transform((val) => (val < env.DEFAULT_MINIMUM_TOKENS ? env.DEFAULT_MINIMUM_TOKENS : val))
47
60
  .optional(),
48
- }, async ({ query, filterByRepoIds: repoIds = [], filterByLanguages: languages = [], maxTokens = env.DEFAULT_MINIMUM_TOKENS, includeCodeSnippets = false, caseSensitive = false, }) => {
49
- if (repoIds.length > 0) {
50
- query += ` ( repo:${repoIds.map(id => escapeStringRegexp(id)).join(' or repo:')} )`;
61
+ }, async ({ query, filterByRepos: repos = [], filterByLanguages: languages = [], filterByFilepaths: filepaths = [], maxTokens = env.DEFAULT_MINIMUM_TOKENS, includeCodeSnippets = false, caseSensitive = false, ref, useRegex = false, }) => {
62
+ if (repos.length > 0) {
63
+ query += ` (repo:${repos.map(id => escapeStringRegexp(id)).join(' or repo:')})`;
51
64
  }
52
65
  if (languages.length > 0) {
53
- query += ` ( lang:${languages.join(' or lang:')} )`;
66
+ query += ` (lang:${languages.join(' or lang:')})`;
67
+ }
68
+ if (filepaths.length > 0) {
69
+ query += ` (file:${filepaths.map(filepath => escapeStringRegexp(filepath)).join(' or file:')})`;
70
+ }
71
+ if (ref) {
72
+ query += ` ( rev:${ref} )`;
54
73
  }
55
74
  const response = await search({
56
75
  query,
57
76
  matches: env.DEFAULT_MATCHES,
58
77
  contextLines: env.DEFAULT_CONTEXT_LINES,
59
- isRegexEnabled: true,
78
+ isRegexEnabled: useRegex,
60
79
  isCaseSensitivityEnabled: caseSensitive,
61
- source: 'mcp'
80
+ source: 'mcp',
62
81
  });
63
- if (isServiceError(response)) {
64
- return {
65
- content: [{
66
- type: "text",
67
- text: `Error searching code: ${response.message}`,
68
- }],
69
- };
70
- }
71
82
  if (response.files.length === 0) {
72
83
  return {
73
84
  content: [{
@@ -81,8 +92,12 @@ server.tool("search_code", `Fetches code that matches the provided regex pattern
81
92
  let isResponseTruncated = false;
82
93
  for (const file of response.files) {
83
94
  const numMatches = file.chunks.reduce((acc, chunk) => acc + chunk.matchRanges.length, 0);
84
- const fileIdentifier = file.webUrl ?? file.fileName.text;
85
- let text = `file: ${fileIdentifier}\nnum_matches: ${numMatches}\nrepository: ${file.repository}\nlanguage: ${file.language}`;
95
+ let text = dedent `
96
+ file: ${file.webUrl}
97
+ num_matches: ${numMatches}
98
+ repo: ${file.repository}
99
+ language: ${file.language}
100
+ `;
86
101
  if (includeCodeSnippets) {
87
102
  const snippets = file.chunks.map(chunk => {
88
103
  return `\`\`\`\n${chunk.content}\n\`\`\``;
@@ -124,76 +139,40 @@ server.tool("search_code", `Fetches code that matches the provided regex pattern
124
139
  content,
125
140
  };
126
141
  });
127
- server.tool("list_repos", "Lists repositories in the organization with optional filtering and pagination. If you receive an error that indicates that you're not authenticated, please inform the user to set the SOURCEBOT_API_KEY environment variable.", listReposRequestSchema.shape, async ({ query, pageNumber = 1, limit = 50 }) => {
128
- const response = await listRepos();
129
- if (isServiceError(response)) {
130
- return {
131
- content: [{
132
- type: "text",
133
- text: `Error listing repositories: ${response.message}`,
134
- }],
135
- };
136
- }
137
- // Apply query filter if provided
138
- let filtered = response;
139
- if (query) {
140
- const lowerQuery = query.toLowerCase();
141
- filtered = response.filter(repo => repo.repoName.toLowerCase().includes(lowerQuery) ||
142
- repo.repoDisplayName?.toLowerCase().includes(lowerQuery));
143
- }
144
- // Sort alphabetically for consistent pagination
145
- filtered.sort((a, b) => a.repoName.localeCompare(b.repoName));
146
- // Apply pagination
147
- const startIndex = (pageNumber - 1) * limit;
148
- const endIndex = startIndex + limit;
149
- const paginated = filtered.slice(startIndex, endIndex);
150
- // Format output
151
- const content = paginated.map(repo => {
152
- const repoUrl = repo.webUrl ?? repo.repoCloneUrl;
153
- return {
154
- type: "text",
155
- text: `id: ${repo.repoName}\nurl: ${repoUrl}`,
156
- };
157
- });
158
- // Add pagination info
159
- if (content.length === 0 && filtered.length > 0) {
160
- content.push({
161
- type: "text",
162
- text: `No results on page ${pageNumber}. Total matching repositories: ${filtered.length}`,
163
- });
164
- }
165
- else if (filtered.length > endIndex) {
166
- content.push({
167
- type: "text",
168
- text: `Showing ${paginated.length} repositories (page ${pageNumber}). Total matching: ${filtered.length}. Use pageNumber ${pageNumber + 1} to see more.`,
169
- });
170
- }
142
+ server.tool("list_commits", dedent `Get a list of commits for a given repository.`, listCommitsQueryParamsSchema.shape, async (request) => {
143
+ const result = await listCommits(request);
171
144
  return {
172
- content,
145
+ content: [{
146
+ type: "text", text: JSON.stringify(result)
147
+ }],
173
148
  };
174
149
  });
175
- server.tool("get_file_source", "Fetches the source code for a given file. If you receive an error that indicates that you're not authenticated, please inform the user to set the SOURCEBOT_API_KEY environment variable.", {
176
- fileName: z.string().describe("The file to fetch the source code for."),
177
- repoId: z.string().describe("The repository to fetch the source code for. This is the Sourcebot compatible repository ID."),
178
- }, async ({ fileName, repoId }) => {
179
- const response = await getFileSource({
180
- fileName,
181
- repository: repoId,
182
- });
183
- if (isServiceError(response)) {
184
- return {
185
- content: [{
186
- type: "text",
187
- text: `Error fetching file source: ${response.message}`,
188
- }],
189
- };
190
- }
191
- const content = [{
192
- type: "text",
193
- text: `file: ${fileName}\nrepository: ${repoId}\nlanguage: ${response.language}\nsource:\n${response.source}`,
194
- }];
150
+ server.tool("list_repos", dedent `Lists repositories in the organization with optional filtering and pagination.`, listReposQueryParamsSchema.shape, async (request) => {
151
+ const result = await listRepos(request);
195
152
  return {
196
- content,
153
+ content: [{
154
+ type: "text", text: JSON.stringify({
155
+ repos: result.repos.map((repo) => ({
156
+ name: repo.repoName,
157
+ url: repo.webUrl,
158
+ pushedAt: repo.pushedAt,
159
+ })),
160
+ totalCount: result.totalCount,
161
+ })
162
+ }]
163
+ };
164
+ });
165
+ server.tool("read_file", dedent `Reads the source code for a given file.`, fileSourceRequestSchema.shape, async (request) => {
166
+ const response = await getFileSource(request);
167
+ return {
168
+ content: [{
169
+ type: "text", text: JSON.stringify({
170
+ source: response.source,
171
+ language: response.language,
172
+ path: response.path,
173
+ url: response.webUrl,
174
+ })
175
+ }]
197
176
  };
198
177
  });
199
178
  const runServer = async () => {