@sourcebot/mcp 1.0.12 → 1.0.13
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 +13 -0
- package/README.md +18 -0
- 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,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.13] - 2026-01-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Added `search_commits` tool to search a repos commit history. [#625](https://github.com/sourcebot-dev/sourcebot/pull/625)
|
|
14
|
+
- Added `gitRevision` parameter to the `search_code` tool to allow for searching on different branches. [#625](https://github.com/sourcebot-dev/sourcebot/pull/625)
|
|
15
|
+
- Added server side pagination support for `list_commits` and `list_repos`. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
|
|
16
|
+
- Added `filterByFilepaths` and `useRegex` params to the `search_code` tool. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Renamed `search_commits` tool to `list_commits`. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
|
|
20
|
+
- Renamed `gitRevision` param to `ref` on `search_code` tool. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
|
|
21
|
+
- Generally improved tool and tool param descriptions for all tools. [#795](https://github.com/sourcebot-dev/sourcebot/pull/795)
|
|
22
|
+
|
|
10
23
|
## [1.0.12] - 2026-01-13
|
|
11
24
|
|
|
12
25
|
### Fixed
|
package/README.md
CHANGED
|
@@ -208,6 +208,24 @@ Fetches the source code for a given file.
|
|
|
208
208
|
| `repoId` | yes | The Sourcebot repository ID. |
|
|
209
209
|
</details>
|
|
210
210
|
|
|
211
|
+
### search_commits
|
|
212
|
+
|
|
213
|
+
Searches for commits in a specific repository based on actual commit time.
|
|
214
|
+
|
|
215
|
+
<details>
|
|
216
|
+
<summary>Parameters</summary>
|
|
217
|
+
|
|
218
|
+
| Name | Required | Description |
|
|
219
|
+
|:-----------|:---------|:-----------------------------------------------------------------------------------------------|
|
|
220
|
+
| `repoId` | yes | Repository identifier: either numeric database ID (e.g., 123) or full repository name (e.g., "github.com/owner/repo") as returned by `list_repos`. |
|
|
221
|
+
| `query` | no | Search query to filter commits by message (case-insensitive). |
|
|
222
|
+
| `since` | no | Show commits after this date (by commit time). Supports ISO 8601 or relative formats. |
|
|
223
|
+
| `until` | no | Show commits before this date (by commit time). Supports ISO 8601 or relative formats. |
|
|
224
|
+
| `author` | no | Filter by author name or email (supports partial matches). |
|
|
225
|
+
| `maxCount` | no | Maximum number of commits to return (default: 50). |
|
|
226
|
+
|
|
227
|
+
</details>
|
|
228
|
+
|
|
211
229
|
|
|
212
230
|
## Supported Code Hosts
|
|
213
231
|
Sourcebot supports the following code hosts:
|
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 () => {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\n// Entry point for the MCP server\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport escapeStringRegexp from 'escape-string-regexp';\nimport { z } from 'zod';\nimport { listRepos, search, getFileSource } from './client.js';\nimport { env, numberSchema } from './env.js';\nimport { listReposRequestSchema } from './schemas.js';\nimport { TextContent } from './types.js';\nimport { isServiceError } from './utils.js';\n\n// Create MCP server\nconst server = new McpServer({\n name: 'sourcebot-mcp-server',\n version: '0.1.0',\n});\n\n\nserver.tool(\n \"search_code\",\n `Fetches code that matches the provided regex pattern in \\`query\\`. This is NOT a semantic search.\n Results are returned as an array of matching files, with the file's URL, repository, and language.\n If you receive an error that indicates that you're not authenticated, please inform the user to set the SOURCEBOT_API_KEY environment variable.\n 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).\n 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.\n **ONLY USE** the \\`filterByRepoIds\\` property if the request requires searching a specific repo(s). Otherwise, leave it empty.`,\n {\n query: z\n .string()\n .describe(`The regex pattern to search for. RULES:\n 1. When a regex special character needs to be escaped, ALWAYS use a single backslash (\\) (e.g., 'console\\.log')\n 2. **ALWAYS** escape spaces with a single backslash (\\) (e.g., 'console\\ log')\n `),\n filterByRepoIds: z\n .array(z.string())\n .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.`)\n .optional(),\n filterByLanguages: z\n .array(z.string())\n .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`)\n .optional(),\n caseSensitive: z\n .boolean()\n .describe(`Whether the search should be case sensitive (default: false).`)\n .optional(),\n includeCodeSnippets: z\n .boolean()\n .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.`)\n .optional(),\n maxTokens: numberSchema\n .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.`)\n .transform((val) => (val < env.DEFAULT_MINIMUM_TOKENS ? env.DEFAULT_MINIMUM_TOKENS : val))\n .optional(),\n },\n async ({\n query,\n filterByRepoIds: repoIds = [],\n filterByLanguages: languages = [],\n maxTokens = env.DEFAULT_MINIMUM_TOKENS,\n includeCodeSnippets = false,\n caseSensitive = false,\n }) => {\n if (repoIds.length > 0) {\n query += ` ( repo:${repoIds.map(id => escapeStringRegexp(id)).join(' or repo:')} )`;\n }\n\n if (languages.length > 0) {\n query += ` ( lang:${languages.join(' or lang:')} )`;\n }\n\n const response = await search({\n query,\n matches: env.DEFAULT_MATCHES,\n contextLines: env.DEFAULT_CONTEXT_LINES,\n isRegexEnabled: true,\n isCaseSensitivityEnabled: caseSensitive,\n source: 'mcp'\n });\n\n if (isServiceError(response)) {\n return {\n content: [{\n type: \"text\",\n text: `Error searching code: ${response.message}`,\n }],\n };\n }\n\n if (response.files.length === 0) {\n return {\n content: [{\n type: \"text\",\n text: `No results found for the query: ${query}`,\n }],\n };\n }\n\n const content: TextContent[] = [];\n let totalTokens = 0;\n let isResponseTruncated = false;\n\n for (const file of response.files) {\n const numMatches = file.chunks.reduce(\n (acc, chunk) => acc + chunk.matchRanges.length,\n 0,\n );\n const fileIdentifier = file.webUrl ?? file.fileName.text;\n let text = `file: ${fileIdentifier}\\nnum_matches: ${numMatches}\\nrepository: ${file.repository}\\nlanguage: ${file.language}`;\n\n if (includeCodeSnippets) {\n const snippets = file.chunks.map(chunk => {\n return `\\`\\`\\`\\n${chunk.content}\\n\\`\\`\\``\n }).join('\\n');\n text += `\\n\\n${snippets}`;\n }\n\n\n // Rough estimate of the number of tokens in the text\n // @see: https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them\n const tokens = text.length / 4;\n\n if ((totalTokens + tokens) > maxTokens) {\n // Calculate remaining token budget\n const remainingTokens = maxTokens - totalTokens;\n\n if (remainingTokens > 100) { // Only truncate if meaningful space left\n // Truncate text to fit remaining tokens (tokens ≈ chars/4)\n const maxLength = Math.floor(remainingTokens * 4);\n const truncatedText = text.substring(0, maxLength) + \"\\n\\n...[content truncated due to token limit]\";\n\n content.push({\n type: \"text\",\n text: truncatedText,\n });\n\n totalTokens += remainingTokens;\n }\n\n isResponseTruncated = true;\n break;\n }\n\n totalTokens += tokens;\n content.push({\n type: \"text\",\n text,\n });\n }\n\n if (isResponseTruncated) {\n content.push({\n type: \"text\",\n text: `The response was truncated because the number of tokens exceeded the maximum limit of ${maxTokens}.`,\n });\n }\n\n return {\n content,\n }\n }\n);\n\nserver.tool(\n \"list_repos\",\n \"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.\",\n listReposRequestSchema.shape,\n async ({ query, pageNumber = 1, limit = 50 }: {\n query?: string;\n pageNumber?: number;\n limit?: number;\n }) => {\n const response = await listRepos();\n if (isServiceError(response)) {\n return {\n content: [{\n type: \"text\",\n text: `Error listing repositories: ${response.message}`,\n }],\n };\n }\n\n // Apply query filter if provided\n let filtered = response;\n if (query) {\n const lowerQuery = query.toLowerCase();\n filtered = response.filter(repo =>\n repo.repoName.toLowerCase().includes(lowerQuery) ||\n repo.repoDisplayName?.toLowerCase().includes(lowerQuery)\n );\n }\n\n // Sort alphabetically for consistent pagination\n filtered.sort((a, b) => a.repoName.localeCompare(b.repoName));\n\n // Apply pagination\n const startIndex = (pageNumber - 1) * limit;\n const endIndex = startIndex + limit;\n const paginated = filtered.slice(startIndex, endIndex);\n\n // Format output\n const content: TextContent[] = paginated.map(repo => {\n const repoUrl = repo.webUrl ?? repo.repoCloneUrl;\n return {\n type: \"text\",\n text: `id: ${repo.repoName}\\nurl: ${repoUrl}`,\n }\n });\n\n // Add pagination info\n if (content.length === 0 && filtered.length > 0) {\n content.push({\n type: \"text\",\n text: `No results on page ${pageNumber}. Total matching repositories: ${filtered.length}`,\n });\n } else if (filtered.length > endIndex) {\n content.push({\n type: \"text\",\n text: `Showing ${paginated.length} repositories (page ${pageNumber}). Total matching: ${filtered.length}. Use pageNumber ${pageNumber + 1} to see more.`,\n });\n }\n\n return {\n content,\n };\n }\n);\n\nserver.tool(\n \"get_file_source\",\n \"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.\",\n {\n fileName: z.string().describe(\"The file to fetch the source code for.\"),\n repoId: z.string().describe(\"The repository to fetch the source code for. This is the Sourcebot compatible repository ID.\"),\n },\n async ({ fileName, repoId }) => {\n const response = await getFileSource({\n fileName,\n repository: repoId,\n });\n\n if (isServiceError(response)) {\n return {\n content: [{\n type: \"text\",\n text: `Error fetching file source: ${response.message}`,\n }],\n };\n }\n\n const content: TextContent[] = [{\n type: \"text\",\n text: `file: ${fileName}\\nrepository: ${repoId}\\nlanguage: ${response.language}\\nsource:\\n${response.source}`,\n }]\n\n return {\n content,\n };\n }\n);\n\n\n\nconst runServer = async () => {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nrunServer().catch((error) => {\n console.error('Failed to start MCP server:', error);\n process.exit(1);\n});\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\n// Entry point for the MCP server\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport _dedent from \"dedent\";\nimport escapeStringRegexp from 'escape-string-regexp';\nimport { z } from 'zod';\nimport { getFileSource, listCommits, listRepos, search } from './client.js';\nimport { env, numberSchema } from './env.js';\nimport { fileSourceRequestSchema, listCommitsQueryParamsSchema, listReposQueryParamsSchema } from './schemas.js';\nimport { FileSourceRequest, ListCommitsQueryParamsSchema, ListReposQueryParams, TextContent } from './types.js';\n\nconst dedent = _dedent.withOptions({ alignValues: true });\n\n// Create MCP server\nconst server = new McpServer({\n name: 'sourcebot-mcp-server',\n version: '0.1.0',\n});\n\n\nserver.tool(\n \"search_code\",\n dedent`\n 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.\n `,\n {\n query: z\n .string()\n .describe(`The search pattern to match against code contents. Do not escape quotes in your query.`)\n // Escape backslashes first, then quotes, and wrap in double quotes\n // so the query is treated as a literal phrase (like grep).\n .transform((val) => {\n const escaped = val.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n return `\"${escaped}\"`;\n }),\n useRegex: z\n .boolean()\n .describe(`Whether to use regular expression matching to match the search query against code contents. When false, substring matching is used. (default: false)`)\n .optional(),\n filterByRepos: z\n .array(z.string())\n .describe(`Scope the search to the provided repositories.`)\n .optional(),\n filterByLanguages: z\n .array(z.string())\n .describe(`Scope the search to the provided languages.`)\n .optional(),\n filterByFilepaths: z\n .array(z.string())\n .describe(`Scope the search to the provided filepaths.`)\n .optional(),\n caseSensitive: z\n .boolean()\n .describe(`Whether the search should be case sensitive (default: false).`)\n .optional(),\n includeCodeSnippets: z\n .boolean()\n .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)`)\n .optional(),\n ref: z\n .string()\n .describe(`Commit SHA, branch or tag name to search on. If not provided, defaults to the default branch (usually 'main' or 'master').`)\n .optional(),\n maxTokens: numberSchema\n .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.`)\n .transform((val) => (val < env.DEFAULT_MINIMUM_TOKENS ? env.DEFAULT_MINIMUM_TOKENS : val))\n .optional(),\n },\n async ({\n query,\n filterByRepos: repos = [],\n filterByLanguages: languages = [],\n filterByFilepaths: filepaths = [],\n maxTokens = env.DEFAULT_MINIMUM_TOKENS,\n includeCodeSnippets = false,\n caseSensitive = false,\n ref,\n useRegex = false,\n }) => {\n if (repos.length > 0) {\n query += ` (repo:${repos.map(id => escapeStringRegexp(id)).join(' or repo:')})`;\n }\n\n if (languages.length > 0) {\n query += ` (lang:${languages.join(' or lang:')})`;\n }\n\n if (filepaths.length > 0) {\n query += ` (file:${filepaths.map(filepath => escapeStringRegexp(filepath)).join(' or file:')})`;\n }\n\n if (ref) {\n query += ` ( rev:${ref} )`;\n }\n\n const response = await search({\n query,\n matches: env.DEFAULT_MATCHES,\n contextLines: env.DEFAULT_CONTEXT_LINES,\n isRegexEnabled: useRegex,\n isCaseSensitivityEnabled: caseSensitive,\n source: 'mcp',\n });\n\n if (response.files.length === 0) {\n return {\n content: [{\n type: \"text\",\n text: `No results found for the query: ${query}`,\n }],\n };\n }\n\n const content: TextContent[] = [];\n let totalTokens = 0;\n let isResponseTruncated = false;\n\n for (const file of response.files) {\n const numMatches = file.chunks.reduce(\n (acc, chunk) => acc + chunk.matchRanges.length,\n 0,\n );\n let text = dedent`\n file: ${file.webUrl}\n num_matches: ${numMatches}\n repo: ${file.repository}\n language: ${file.language}\n `;\n\n if (includeCodeSnippets) {\n const snippets = file.chunks.map(chunk => {\n return `\\`\\`\\`\\n${chunk.content}\\n\\`\\`\\``\n }).join('\\n');\n text += `\\n\\n${snippets}`;\n }\n\n\n // Rough estimate of the number of tokens in the text\n // @see: https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them\n const tokens = text.length / 4;\n\n if ((totalTokens + tokens) > maxTokens) {\n // Calculate remaining token budget\n const remainingTokens = maxTokens - totalTokens;\n\n if (remainingTokens > 100) { // Only truncate if meaningful space left\n // Truncate text to fit remaining tokens (tokens ≈ chars/4)\n const maxLength = Math.floor(remainingTokens * 4);\n const truncatedText = text.substring(0, maxLength) + \"\\n\\n...[content truncated due to token limit]\";\n\n content.push({\n type: \"text\",\n text: truncatedText,\n });\n\n totalTokens += remainingTokens;\n }\n\n isResponseTruncated = true;\n break;\n }\n\n totalTokens += tokens;\n content.push({\n type: \"text\",\n text,\n });\n }\n\n if (isResponseTruncated) {\n content.push({\n type: \"text\",\n text: `The response was truncated because the number of tokens exceeded the maximum limit of ${maxTokens}.`,\n });\n }\n\n return {\n content,\n }\n }\n);\n\nserver.tool(\n \"list_commits\",\n dedent`Get a list of commits for a given repository.`,\n listCommitsQueryParamsSchema.shape,\n async (request: ListCommitsQueryParamsSchema) => {\n const result = await listCommits(request);\n\n return {\n content: [{\n type: \"text\", text: JSON.stringify(result)\n }],\n };\n }\n);\n\nserver.tool(\n \"list_repos\",\n dedent`Lists repositories in the organization with optional filtering and pagination.`,\n listReposQueryParamsSchema.shape,\n async (request: ListReposQueryParams) => {\n const result = await listRepos(request);\n\n return {\n content: [{\n type: \"text\", text: JSON.stringify({\n repos: result.repos.map((repo) => ({\n name: repo.repoName,\n url: repo.webUrl,\n pushedAt: repo.pushedAt,\n })),\n totalCount: result.totalCount,\n })\n }]\n };\n }\n);\n\nserver.tool(\n \"read_file\",\n dedent`Reads the source code for a given file.`,\n fileSourceRequestSchema.shape,\n async (request: FileSourceRequest) => {\n const response = await getFileSource(request);\n\n return {\n content: [{\n type: \"text\", text: JSON.stringify({\n source: response.source,\n language: response.language,\n path: response.path,\n url: response.webUrl,\n })\n }]\n };\n }\n);\n\n\n\nconst runServer = async () => {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nrunServer().catch((error) => {\n console.error('Failed to start MCP server:', error);\n process.exit(1);\n});\n"]}
|