@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 +18 -0
- package/README.md +44 -18
- package/dist/client.d.ts +124 -4
- package/dist/client.js +63 -23
- package/dist/client.js.map +1 -1
- package/dist/index.js +78 -99
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.ts +128 -34
- package/dist/schemas.js +94 -15
- package/dist/schemas.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +4 -2
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +7 -0
- package/dist/utils.js.map +1 -1
- package/package.json +2 -1
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
|
-
|
|
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 |
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
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
|
|
191
|
-
|
|
192
|
-
| `query`
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
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
|
-
###
|
|
203
|
+
### read_file
|
|
199
204
|
|
|
200
|
-
|
|
205
|
+
Reads the source code for a given file.
|
|
201
206
|
|
|
202
207
|
<details>
|
|
203
208
|
<summary>Parameters</summary>
|
|
204
209
|
|
|
205
|
-
| Name
|
|
206
|
-
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
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,
|
|
2
|
-
export declare const search: (request: SearchRequest) => Promise<
|
|
3
|
-
|
|
4
|
-
|
|
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 {
|
|
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
|
|
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
|
-
})
|
|
13
|
-
|
|
14
|
-
return result;
|
|
15
|
-
}
|
|
16
|
-
return searchResponseSchema.parse(result);
|
|
31
|
+
});
|
|
32
|
+
return parseResponse(response, searchResponseSchema);
|
|
17
33
|
};
|
|
18
|
-
export const listRepos = async () => {
|
|
19
|
-
const
|
|
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
|
-
})
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
33
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
package/dist/client.js.map
CHANGED
|
@@ -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,
|
|
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
|
|
8
|
+
import { getFileSource, listCommits, listRepos, search } from './client.js';
|
|
8
9
|
import { env, numberSchema } from './env.js';
|
|
9
|
-
import {
|
|
10
|
-
|
|
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", `
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
49
|
-
if (
|
|
50
|
-
query += ` (
|
|
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 += ` (
|
|
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:
|
|
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
|
-
|
|
85
|
-
|
|
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("
|
|
128
|
-
const
|
|
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("
|
|
176
|
-
|
|
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 () => {
|