codewiki-mcp 1.0.0 → 1.1.0
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/LICENSE +21 -0
- package/README.md +206 -144
- package/README.ru.md +206 -144
- package/dist/cli.js +0 -0
- package/dist/schemas.d.ts +12 -38
- package/package.json +26 -4
- package/.claude/skills/codewiki.md +0 -186
- package/.cursor/mcp.json +0 -9
- package/.dockerignore +0 -10
- package/.github/dependabot.yml +0 -33
- package/.github/workflows/ci.yml +0 -42
- package/.github/workflows/release.yml +0 -36
- package/.releaserc.json +0 -16
- package/CHANGELOG.md +0 -6
- package/Dockerfile +0 -15
- package/src/cli.ts +0 -67
- package/src/index.ts +0 -32
- package/src/lib/batchexecute.ts +0 -72
- package/src/lib/codewikiClient.ts +0 -294
- package/src/lib/config.ts +0 -30
- package/src/lib/errors.ts +0 -57
- package/src/lib/extractKeyword.ts +0 -47
- package/src/lib/repo.ts +0 -70
- package/src/lib/resolveRepo.ts +0 -60
- package/src/schemas.ts +0 -22
- package/src/server.ts +0 -120
- package/src/tools/askRepo.ts +0 -38
- package/src/tools/fetchRepo.ts +0 -74
- package/src/tools/searchRepos.ts +0 -40
- package/tests/batchexecute.test.ts +0 -42
- package/tests/client.test.ts +0 -129
- package/tests/errors.test.ts +0 -122
- package/tests/extractKeyword.test.ts +0 -34
- package/tests/resolveRepo.test.ts +0 -79
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -10
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import type { CodeWikiConfig } from './config.js'
|
|
2
|
-
import { extractRpcPayload } from './batchexecute.js'
|
|
3
|
-
import { CodeWikiError } from './errors.js'
|
|
4
|
-
import { normalizeRepoInput } from './repo.js'
|
|
5
|
-
|
|
6
|
-
export interface CodeWikiClientOptions {
|
|
7
|
-
baseUrl?: string
|
|
8
|
-
timeoutMs?: number
|
|
9
|
-
maxRetries?: number
|
|
10
|
-
retryDelay?: number
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface SearchRepoResult {
|
|
14
|
-
fullName: string
|
|
15
|
-
url: string | null
|
|
16
|
-
description: string | null
|
|
17
|
-
avatarUrl: string | null
|
|
18
|
-
extraScore: number | null
|
|
19
|
-
raw: unknown
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface WikiSection {
|
|
23
|
-
title: string
|
|
24
|
-
level: number
|
|
25
|
-
summary: string | null
|
|
26
|
-
markdown: string
|
|
27
|
-
anchor: string | null
|
|
28
|
-
diagramCount: number
|
|
29
|
-
raw: unknown
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface FetchRepositoryResult {
|
|
33
|
-
repo: string
|
|
34
|
-
commit: string | null
|
|
35
|
-
canonicalUrl: string | null
|
|
36
|
-
sections: WikiSection[]
|
|
37
|
-
raw: unknown
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface AskHistoryItem {
|
|
41
|
-
role: 'user' | 'assistant'
|
|
42
|
-
content: string
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface ResponseMeta {
|
|
46
|
-
totalBytes: number
|
|
47
|
-
totalElapsedMs: number
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface WithMeta<T> {
|
|
51
|
-
data: T
|
|
52
|
-
meta: ResponseMeta
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export class CodeWikiClient {
|
|
56
|
-
private readonly baseUrl: string
|
|
57
|
-
private readonly timeoutMs: number
|
|
58
|
-
private readonly maxRetries: number
|
|
59
|
-
private readonly retryDelay: number
|
|
60
|
-
|
|
61
|
-
constructor(options: CodeWikiClientOptions = {}) {
|
|
62
|
-
this.baseUrl = options.baseUrl ?? 'https://codewiki.google'
|
|
63
|
-
this.timeoutMs = options.timeoutMs ?? 30_000
|
|
64
|
-
this.maxRetries = options.maxRetries ?? 3
|
|
65
|
-
this.retryDelay = options.retryDelay ?? 250
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
static fromConfig(config: CodeWikiConfig): CodeWikiClient {
|
|
69
|
-
return new CodeWikiClient({
|
|
70
|
-
baseUrl: config.baseUrl,
|
|
71
|
-
timeoutMs: config.requestTimeout,
|
|
72
|
-
maxRetries: config.maxRetries,
|
|
73
|
-
retryDelay: config.retryDelay,
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async searchRepositories(query: string, limit = 10): Promise<WithMeta<SearchRepoResult[]>> {
|
|
78
|
-
const start = performance.now()
|
|
79
|
-
const { payload, bytes } = await this.callRpc('vyWDAf', [query, limit, query, 0], { sourcePath: '/' })
|
|
80
|
-
|
|
81
|
-
const rows = Array.isArray(payload) && Array.isArray(payload[0])
|
|
82
|
-
? payload[0]
|
|
83
|
-
: []
|
|
84
|
-
|
|
85
|
-
const data = rows
|
|
86
|
-
.filter((item): item is unknown[] => Array.isArray(item))
|
|
87
|
-
.map((item) => {
|
|
88
|
-
const fullName = typeof item[0] === 'string' ? item[0] : 'unknown/unknown'
|
|
89
|
-
const url = Array.isArray(item[3]) && typeof item[3][1] === 'string' ? item[3][1] : null
|
|
90
|
-
|
|
91
|
-
let description: string | null = null
|
|
92
|
-
let avatarUrl: string | null = null
|
|
93
|
-
let extraScore: number | null = null
|
|
94
|
-
if (Array.isArray(item[5])) {
|
|
95
|
-
description = typeof item[5][0] === 'string' ? item[5][0] : null
|
|
96
|
-
avatarUrl = typeof item[5][1] === 'string' ? item[5][1] : null
|
|
97
|
-
extraScore = typeof item[5][2] === 'number' ? item[5][2] : null
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
fullName,
|
|
102
|
-
url,
|
|
103
|
-
description,
|
|
104
|
-
avatarUrl,
|
|
105
|
-
extraScore,
|
|
106
|
-
raw: item,
|
|
107
|
-
}
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
data,
|
|
112
|
-
meta: { totalBytes: bytes, totalElapsedMs: Math.round(performance.now() - start) },
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async fetchRepository(repoInput: string): Promise<WithMeta<FetchRepositoryResult>> {
|
|
117
|
-
const start = performance.now()
|
|
118
|
-
const repo = normalizeRepoInput(repoInput)
|
|
119
|
-
|
|
120
|
-
const { payload, bytes } = await this.callRpc('VSX6ub', [repo.repoUrl], {
|
|
121
|
-
sourcePath: repo.sourcePath,
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
const root = Array.isArray(payload) ? payload : []
|
|
125
|
-
const primary = Array.isArray(root[0]) ? root[0] : []
|
|
126
|
-
const repoInfo = Array.isArray(primary[0]) ? primary[0] : []
|
|
127
|
-
const sectionsRaw = Array.isArray(primary[1]) ? primary[1] : []
|
|
128
|
-
|
|
129
|
-
const canonicalUrl =
|
|
130
|
-
Array.isArray(root[1])
|
|
131
|
-
&& Array.isArray(root[1][0])
|
|
132
|
-
&& typeof root[1][0][1] === 'string'
|
|
133
|
-
? root[1][0][1]
|
|
134
|
-
: null
|
|
135
|
-
|
|
136
|
-
const repoName = typeof repoInfo[0] === 'string' ? repoInfo[0] : repo.repoPath
|
|
137
|
-
const commit = typeof repoInfo[1] === 'string' ? repoInfo[1] : null
|
|
138
|
-
|
|
139
|
-
const sections: WikiSection[] = sectionsRaw
|
|
140
|
-
.filter((item): item is unknown[] => Array.isArray(item))
|
|
141
|
-
.map((item) => {
|
|
142
|
-
const title = typeof item[0] === 'string' ? item[0] : 'Untitled'
|
|
143
|
-
|
|
144
|
-
const rawLevel = item[1]
|
|
145
|
-
const level = typeof rawLevel === 'number' && Number.isFinite(rawLevel)
|
|
146
|
-
? Math.max(1, Math.min(6, Math.floor(rawLevel)))
|
|
147
|
-
: 1
|
|
148
|
-
|
|
149
|
-
const summary = typeof item[2] === 'string' ? item[2] : null
|
|
150
|
-
const markdown = typeof item[5] === 'string'
|
|
151
|
-
? item[5]
|
|
152
|
-
: typeof item[4] === 'string'
|
|
153
|
-
? item[4]
|
|
154
|
-
: (summary ?? '')
|
|
155
|
-
|
|
156
|
-
let diagramCount = 0
|
|
157
|
-
if (Array.isArray(item[7])) {
|
|
158
|
-
diagramCount = item[7].length
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const anchor = [...item]
|
|
162
|
-
.reverse()
|
|
163
|
-
.find((value): value is string => typeof value === 'string' && value.startsWith('#')) ?? null
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
title,
|
|
167
|
-
level,
|
|
168
|
-
summary,
|
|
169
|
-
markdown,
|
|
170
|
-
anchor,
|
|
171
|
-
diagramCount,
|
|
172
|
-
raw: item,
|
|
173
|
-
}
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
data: {
|
|
178
|
-
repo: repoName,
|
|
179
|
-
commit,
|
|
180
|
-
canonicalUrl,
|
|
181
|
-
sections,
|
|
182
|
-
raw: payload,
|
|
183
|
-
},
|
|
184
|
-
meta: { totalBytes: bytes, totalElapsedMs: Math.round(performance.now() - start) },
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async askRepository(repoInput: string, question: string, history: AskHistoryItem[] = []): Promise<WithMeta<string>> {
|
|
189
|
-
const start = performance.now()
|
|
190
|
-
const repo = normalizeRepoInput(repoInput)
|
|
191
|
-
|
|
192
|
-
const messages: [string, 'user' | 'model'][] = [
|
|
193
|
-
...history.map(item => [item.content, item.role === 'assistant' ? 'model' : 'user'] as [string, 'user' | 'model']),
|
|
194
|
-
[question, 'user'],
|
|
195
|
-
]
|
|
196
|
-
|
|
197
|
-
const { payload, bytes } = await this.callRpc('EgIxfe', [messages, [null, repo.repoUrl]], {
|
|
198
|
-
sourcePath: repo.sourcePath,
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
let data: string
|
|
202
|
-
if (Array.isArray(payload) && typeof payload[0] === 'string') {
|
|
203
|
-
data = payload[0]
|
|
204
|
-
} else if (typeof payload === 'string') {
|
|
205
|
-
data = payload
|
|
206
|
-
} else {
|
|
207
|
-
data = JSON.stringify(payload, null, 2)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
data,
|
|
212
|
-
meta: { totalBytes: bytes, totalElapsedMs: Math.round(performance.now() - start) },
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private async callRpc(
|
|
217
|
-
rpcId: string,
|
|
218
|
-
rpcPayload: unknown,
|
|
219
|
-
options: { sourcePath?: string } = {},
|
|
220
|
-
): Promise<{ payload: unknown; bytes: number }> {
|
|
221
|
-
const url = new URL(`${this.baseUrl}/_/BoqAngularSdlcAgentsUi/data/batchexecute`)
|
|
222
|
-
url.searchParams.set('rpcids', rpcId)
|
|
223
|
-
url.searchParams.set('rt', 'c')
|
|
224
|
-
|
|
225
|
-
if (options.sourcePath) {
|
|
226
|
-
url.searchParams.set('source-path', options.sourcePath)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const bodyObject = [[[rpcId, JSON.stringify(rpcPayload), null, 'generic']]]
|
|
230
|
-
const body = `f.req=${encodeURIComponent(JSON.stringify(bodyObject))}&`
|
|
231
|
-
|
|
232
|
-
let lastError: unknown
|
|
233
|
-
|
|
234
|
-
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
235
|
-
if (attempt > 0) {
|
|
236
|
-
const delay = this.retryDelay * Math.pow(2, attempt - 1)
|
|
237
|
-
await new Promise(resolve => setTimeout(resolve, delay))
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const abortController = new AbortController()
|
|
241
|
-
const timeout = setTimeout(() => abortController.abort(), this.timeoutMs)
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
const response = await fetch(url, {
|
|
245
|
-
method: 'POST',
|
|
246
|
-
headers: {
|
|
247
|
-
'content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
248
|
-
},
|
|
249
|
-
body,
|
|
250
|
-
signal: abortController.signal,
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
if (!response.ok) {
|
|
254
|
-
throw new CodeWikiError('RPC_FAIL', `CodeWiki RPC ${rpcId} failed with status ${response.status}`, {
|
|
255
|
-
statusCode: response.status,
|
|
256
|
-
rpcId,
|
|
257
|
-
})
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const text = await response.text()
|
|
261
|
-
const bytes = Buffer.byteLength(text, 'utf8')
|
|
262
|
-
const payload = extractRpcPayload(text, rpcId)
|
|
263
|
-
return { payload, bytes }
|
|
264
|
-
} catch (err) {
|
|
265
|
-
lastError = err
|
|
266
|
-
|
|
267
|
-
if (err instanceof CodeWikiError && err.code === 'RPC_FAIL' && err.statusCode && err.statusCode < 500) {
|
|
268
|
-
throw err
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
272
|
-
lastError = new CodeWikiError('TIMEOUT', `CodeWiki RPC ${rpcId} timed out after ${this.timeoutMs}ms`, {
|
|
273
|
-
rpcId,
|
|
274
|
-
cause: err,
|
|
275
|
-
})
|
|
276
|
-
if (attempt === this.maxRetries) throw lastError
|
|
277
|
-
continue
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (attempt === this.maxRetries) {
|
|
281
|
-
if (err instanceof CodeWikiError) throw err
|
|
282
|
-
throw new CodeWikiError('RPC_FAIL', `CodeWiki RPC ${rpcId} failed: ${err instanceof Error ? err.message : String(err)}`, {
|
|
283
|
-
rpcId,
|
|
284
|
-
cause: err,
|
|
285
|
-
})
|
|
286
|
-
}
|
|
287
|
-
} finally {
|
|
288
|
-
clearTimeout(timeout)
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
throw lastError
|
|
293
|
-
}
|
|
294
|
-
}
|
package/src/lib/config.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
export interface CodeWikiConfig {
|
|
2
|
-
baseUrl: string
|
|
3
|
-
requestTimeout: number
|
|
4
|
-
maxRetries: number
|
|
5
|
-
retryDelay: number
|
|
6
|
-
githubToken?: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const defaults: CodeWikiConfig = {
|
|
10
|
-
baseUrl: 'https://codewiki.google',
|
|
11
|
-
requestTimeout: 30_000,
|
|
12
|
-
maxRetries: 3,
|
|
13
|
-
retryDelay: 250,
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function loadConfig(env: Record<string, string | undefined> = process.env): CodeWikiConfig {
|
|
17
|
-
return {
|
|
18
|
-
baseUrl: env.CODEWIKI_BASE_URL ?? defaults.baseUrl,
|
|
19
|
-
requestTimeout: parsePositiveInt(env.CODEWIKI_REQUEST_TIMEOUT, defaults.requestTimeout),
|
|
20
|
-
maxRetries: parsePositiveInt(env.CODEWIKI_MAX_RETRIES, defaults.maxRetries),
|
|
21
|
-
retryDelay: parsePositiveInt(env.CODEWIKI_RETRY_DELAY, defaults.retryDelay),
|
|
22
|
-
githubToken: env.GITHUB_TOKEN || undefined,
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function parsePositiveInt(value: string | undefined, fallback: number): number {
|
|
27
|
-
if (value === undefined) return fallback
|
|
28
|
-
const n = Number.parseInt(value, 10)
|
|
29
|
-
return Number.isFinite(n) && n > 0 ? n : fallback
|
|
30
|
-
}
|
package/src/lib/errors.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
export type ErrorCode = 'VALIDATION' | 'RPC_FAIL' | 'TIMEOUT' | 'NLP_RESOLVE_FAIL'
|
|
2
|
-
|
|
3
|
-
export class CodeWikiError extends Error {
|
|
4
|
-
readonly code: ErrorCode
|
|
5
|
-
readonly statusCode: number | undefined
|
|
6
|
-
readonly rpcId: string | undefined
|
|
7
|
-
|
|
8
|
-
constructor(
|
|
9
|
-
code: ErrorCode,
|
|
10
|
-
message: string,
|
|
11
|
-
options?: { statusCode?: number; rpcId?: string; cause?: unknown },
|
|
12
|
-
) {
|
|
13
|
-
super(message, { cause: options?.cause })
|
|
14
|
-
this.name = 'CodeWikiError'
|
|
15
|
-
this.code = code
|
|
16
|
-
this.statusCode = options?.statusCode
|
|
17
|
-
this.rpcId = options?.rpcId
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ErrorEnvelope {
|
|
22
|
-
error: {
|
|
23
|
-
code: ErrorCode
|
|
24
|
-
message: string
|
|
25
|
-
rpcId?: string
|
|
26
|
-
statusCode?: number
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function formatMcpError(err: unknown): { content: { type: 'text'; text: string }[]; isError: true } {
|
|
31
|
-
if (err instanceof CodeWikiError) {
|
|
32
|
-
const envelope: ErrorEnvelope = {
|
|
33
|
-
error: {
|
|
34
|
-
code: err.code,
|
|
35
|
-
message: err.message,
|
|
36
|
-
...(err.rpcId ? { rpcId: err.rpcId } : {}),
|
|
37
|
-
...(err.statusCode ? { statusCode: err.statusCode } : {}),
|
|
38
|
-
},
|
|
39
|
-
}
|
|
40
|
-
return {
|
|
41
|
-
content: [{ type: 'text', text: JSON.stringify(envelope, null, 2) }],
|
|
42
|
-
isError: true,
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
47
|
-
const envelope: ErrorEnvelope = {
|
|
48
|
-
error: {
|
|
49
|
-
code: 'RPC_FAIL',
|
|
50
|
-
message,
|
|
51
|
-
},
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
content: [{ type: 'text', text: JSON.stringify(envelope, null, 2) }],
|
|
55
|
-
isError: true,
|
|
56
|
-
}
|
|
57
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import winkNLP from 'wink-nlp'
|
|
2
|
-
import model from 'wink-eng-lite-web-model'
|
|
3
|
-
|
|
4
|
-
const nlp = winkNLP(model)
|
|
5
|
-
const its = nlp.its
|
|
6
|
-
|
|
7
|
-
const STOP_WORDS = new Set([
|
|
8
|
-
'repo', 'repository', 'library', 'framework', 'package', 'module',
|
|
9
|
-
'project', 'tool', 'code', 'source', 'open', 'github', 'gitlab',
|
|
10
|
-
'the', 'a', 'an', 'this', 'that', 'about', 'for', 'with', 'from',
|
|
11
|
-
'how', 'what', 'where', 'which', 'who', 'when', 'why', 'does',
|
|
12
|
-
'show', 'tell', 'me', 'find', 'search', 'get', 'fetch', 'look',
|
|
13
|
-
'please', 'can', 'could', 'would', 'should', 'use', 'using',
|
|
14
|
-
'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
15
|
-
'have', 'has', 'had', 'do', 'did', 'will', 'shall',
|
|
16
|
-
'of', 'in', 'to', 'on', 'at', 'by', 'up', 'it', 'its',
|
|
17
|
-
])
|
|
18
|
-
|
|
19
|
-
const SKIP_POS = new Set(['PUNCT', 'SPACE', 'DET', 'ADP', 'CCONJ', 'SCONJ', 'AUX', 'PART', 'INTJ'])
|
|
20
|
-
|
|
21
|
-
export function extractKeyword(text: string): string | null {
|
|
22
|
-
const doc = nlp.readDoc(text)
|
|
23
|
-
const tokens = doc.tokens()
|
|
24
|
-
|
|
25
|
-
// First pass: look for NOUN/PROPN that aren't stop words
|
|
26
|
-
const nouns: string[] = []
|
|
27
|
-
const fallback: string[] = []
|
|
28
|
-
|
|
29
|
-
tokens.each((token: any) => {
|
|
30
|
-
const pos = token.out(its.pos) as string
|
|
31
|
-
const value = token.out(its.value) as string
|
|
32
|
-
const lower = value.toLowerCase()
|
|
33
|
-
|
|
34
|
-
if (SKIP_POS.has(pos) || value.length <= 1) return
|
|
35
|
-
|
|
36
|
-
if (!STOP_WORDS.has(lower)) {
|
|
37
|
-
if (pos === 'NOUN' || pos === 'PROPN') {
|
|
38
|
-
nouns.push(value)
|
|
39
|
-
} else {
|
|
40
|
-
fallback.push(value)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
// Prefer nouns, then fall back to any non-stop-word token
|
|
46
|
-
return nouns[0] ?? fallback[0] ?? null
|
|
47
|
-
}
|
package/src/lib/repo.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { CodeWikiConfig } from './config.js'
|
|
2
|
-
import { extractKeyword } from './extractKeyword.js'
|
|
3
|
-
import { resolveRepoFromGitHub } from './resolveRepo.js'
|
|
4
|
-
|
|
5
|
-
export interface NormalizedRepo {
|
|
6
|
-
host: string
|
|
7
|
-
repoPath: string
|
|
8
|
-
repoUrl: string
|
|
9
|
-
sourcePath: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function normalizeRepoInput(input: string): NormalizedRepo {
|
|
13
|
-
const raw = input.trim()
|
|
14
|
-
if (!raw) {
|
|
15
|
-
throw new Error('Repository input is empty')
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (/^https?:\/\//i.test(raw)) {
|
|
19
|
-
const url = new URL(raw)
|
|
20
|
-
const repoPath = url.pathname.replace(/^\/+|\/+$/g, '')
|
|
21
|
-
const parts = repoPath.split('/').filter(Boolean)
|
|
22
|
-
if (parts.length < 2) {
|
|
23
|
-
throw new Error('Expected repository URL in the format https://host/owner/repo')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const normalizedPath = `${parts[0]}/${parts[1]}`
|
|
27
|
-
return {
|
|
28
|
-
host: url.hostname,
|
|
29
|
-
repoPath: normalizedPath,
|
|
30
|
-
repoUrl: `https://${url.hostname}/${normalizedPath}`,
|
|
31
|
-
sourcePath: `/${url.hostname}/${normalizedPath}`,
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const parts = raw.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean)
|
|
36
|
-
if (parts.length !== 2) {
|
|
37
|
-
throw new Error('Expected repository in owner/repo format or full URL')
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const repoPath = `${parts[0]}/${parts[1]}`
|
|
41
|
-
return {
|
|
42
|
-
host: 'github.com',
|
|
43
|
-
repoPath,
|
|
44
|
-
repoUrl: `https://github.com/${repoPath}`,
|
|
45
|
-
sourcePath: `/github.com/${repoPath}`,
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function resolveRepoInput(
|
|
50
|
-
input: string,
|
|
51
|
-
config?: Pick<CodeWikiConfig, 'githubToken' | 'requestTimeout'>,
|
|
52
|
-
): Promise<NormalizedRepo> {
|
|
53
|
-
const raw = input.trim()
|
|
54
|
-
|
|
55
|
-
// URL → sync
|
|
56
|
-
if (/^https?:\/\//i.test(raw)) {
|
|
57
|
-
return normalizeRepoInput(raw)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// owner/repo → sync
|
|
61
|
-
const parts = raw.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean)
|
|
62
|
-
if (parts.length === 2) {
|
|
63
|
-
return normalizeRepoInput(raw)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Natural language → NLP → GitHub search → normalize
|
|
67
|
-
const keyword = extractKeyword(raw) ?? raw
|
|
68
|
-
const fullName = await resolveRepoFromGitHub(keyword, config ?? { requestTimeout: 30_000 })
|
|
69
|
-
return normalizeRepoInput(fullName)
|
|
70
|
-
}
|
package/src/lib/resolveRepo.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { CodeWikiConfig } from './config.js'
|
|
2
|
-
import { CodeWikiError } from './errors.js'
|
|
3
|
-
|
|
4
|
-
interface GitHubSearchItem {
|
|
5
|
-
full_name: string
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
interface GitHubSearchResponse {
|
|
9
|
-
items?: GitHubSearchItem[]
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function resolveRepoFromGitHub(
|
|
13
|
-
keyword: string,
|
|
14
|
-
config: Pick<CodeWikiConfig, 'githubToken' | 'requestTimeout'>,
|
|
15
|
-
): Promise<string> {
|
|
16
|
-
const url = new URL('https://api.github.com/search/repositories')
|
|
17
|
-
url.searchParams.set('q', keyword)
|
|
18
|
-
url.searchParams.set('sort', 'stars')
|
|
19
|
-
url.searchParams.set('per_page', '1')
|
|
20
|
-
|
|
21
|
-
const headers: Record<string, string> = {
|
|
22
|
-
'Accept': 'application/vnd.github+json',
|
|
23
|
-
'User-Agent': 'codewiki-mcp',
|
|
24
|
-
}
|
|
25
|
-
if (config.githubToken) {
|
|
26
|
-
headers['Authorization'] = `Bearer ${config.githubToken}`
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const abortController = new AbortController()
|
|
30
|
-
const timeout = setTimeout(() => abortController.abort(), config.requestTimeout)
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
const response = await fetch(url, {
|
|
34
|
-
headers,
|
|
35
|
-
signal: abortController.signal,
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
if (!response.ok) {
|
|
39
|
-
throw new CodeWikiError('NLP_RESOLVE_FAIL', `GitHub search failed with status ${response.status}`, {
|
|
40
|
-
statusCode: response.status,
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const body = await response.json() as GitHubSearchResponse
|
|
45
|
-
const firstItem = body.items?.[0]
|
|
46
|
-
|
|
47
|
-
if (!firstItem) {
|
|
48
|
-
throw new CodeWikiError('NLP_RESOLVE_FAIL', `No GitHub repository found for "${keyword}"`)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return firstItem.full_name
|
|
52
|
-
} catch (err) {
|
|
53
|
-
if (err instanceof CodeWikiError) throw err
|
|
54
|
-
throw new CodeWikiError('NLP_RESOLVE_FAIL', `Failed to resolve "${keyword}": ${err instanceof Error ? err.message : String(err)}`, {
|
|
55
|
-
cause: err,
|
|
56
|
-
})
|
|
57
|
-
} finally {
|
|
58
|
-
clearTimeout(timeout)
|
|
59
|
-
}
|
|
60
|
-
}
|
package/src/schemas.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
export type { ErrorEnvelope } from './lib/errors.js'
|
|
4
|
-
|
|
5
|
-
export const SearchReposInput = z.object({
|
|
6
|
-
query: z.string().min(1),
|
|
7
|
-
limit: z.number().int().min(1).max(50).default(10),
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
export const FetchRepoInput = z.object({
|
|
11
|
-
repo: z.string().min(1),
|
|
12
|
-
mode: z.enum(['aggregate', 'pages']).default('aggregate'),
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
export const AskRepoInput = z.object({
|
|
16
|
-
repo: z.string().min(1),
|
|
17
|
-
question: z.string().min(1),
|
|
18
|
-
history: z.array(z.object({
|
|
19
|
-
role: z.enum(['user', 'assistant']),
|
|
20
|
-
content: z.string().min(1),
|
|
21
|
-
})).max(20).optional(),
|
|
22
|
-
})
|
package/src/server.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { createServer, type Server } from 'node:http'
|
|
2
|
-
import { randomUUID } from 'node:crypto'
|
|
3
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
4
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
5
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
|
|
6
|
-
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
|
|
7
|
-
import type { CodeWikiConfig } from './lib/config.js'
|
|
8
|
-
import { CodeWikiClient } from './lib/codewikiClient.js'
|
|
9
|
-
import { registerSearchReposTool } from './tools/searchRepos.js'
|
|
10
|
-
import { registerFetchRepoTool } from './tools/fetchRepo.js'
|
|
11
|
-
import { registerAskRepoTool } from './tools/askRepo.js'
|
|
12
|
-
|
|
13
|
-
export interface ServerOptions {
|
|
14
|
-
transport: 'stdio' | 'http' | 'sse'
|
|
15
|
-
port?: number
|
|
16
|
-
endpoint?: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const VERSION = '0.2.0'
|
|
20
|
-
|
|
21
|
-
export function createMcpServer(config: CodeWikiConfig): McpServer {
|
|
22
|
-
const mcp = new McpServer({
|
|
23
|
-
name: 'codewiki-mcp',
|
|
24
|
-
version: VERSION,
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
const client = CodeWikiClient.fromConfig(config)
|
|
28
|
-
|
|
29
|
-
registerSearchReposTool(mcp, client)
|
|
30
|
-
registerFetchRepoTool(mcp, client, config)
|
|
31
|
-
registerAskRepoTool(mcp, client, config)
|
|
32
|
-
|
|
33
|
-
return mcp
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let httpServer: Server | undefined
|
|
37
|
-
|
|
38
|
-
export async function startServer(mcp: McpServer, options: ServerOptions): Promise<void> {
|
|
39
|
-
const { transport, port = 3000, endpoint = '/mcp' } = options
|
|
40
|
-
|
|
41
|
-
if (transport === 'stdio') {
|
|
42
|
-
const stdioTransport = new StdioServerTransport()
|
|
43
|
-
await mcp.connect(stdioTransport)
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (transport === 'http') {
|
|
48
|
-
const httpTransport = new StreamableHTTPServerTransport({
|
|
49
|
-
sessionIdGenerator: () => randomUUID(),
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
httpServer = createServer(async (req, res) => {
|
|
53
|
-
if (req.url === endpoint) {
|
|
54
|
-
await httpTransport.handleRequest(req, res)
|
|
55
|
-
} else {
|
|
56
|
-
res.writeHead(404).end('Not Found')
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
await mcp.connect(httpTransport)
|
|
61
|
-
|
|
62
|
-
await new Promise<void>((resolve) => {
|
|
63
|
-
httpServer!.listen(port, () => {
|
|
64
|
-
console.error(`codewiki-mcp HTTP server listening on http://localhost:${port}${endpoint}`)
|
|
65
|
-
resolve()
|
|
66
|
-
})
|
|
67
|
-
})
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (transport === 'sse') {
|
|
72
|
-
const sessions = new Map<string, SSEServerTransport>()
|
|
73
|
-
|
|
74
|
-
httpServer = createServer(async (req, res) => {
|
|
75
|
-
const url = new URL(req.url ?? '/', `http://localhost:${port}`)
|
|
76
|
-
|
|
77
|
-
if (url.pathname === endpoint && req.method === 'GET') {
|
|
78
|
-
const sseTransport = new SSEServerTransport(`${endpoint}/message`, res)
|
|
79
|
-
sessions.set(sseTransport.sessionId, sseTransport)
|
|
80
|
-
|
|
81
|
-
sseTransport.onclose = () => {
|
|
82
|
-
sessions.delete(sseTransport.sessionId)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
await mcp.connect(sseTransport)
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (url.pathname === `${endpoint}/message` && req.method === 'POST') {
|
|
90
|
-
const sessionId = url.searchParams.get('sessionId')
|
|
91
|
-
const sseTransport = sessionId ? sessions.get(sessionId) : undefined
|
|
92
|
-
if (!sseTransport) {
|
|
93
|
-
res.writeHead(400).end('Invalid or missing sessionId')
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
await sseTransport.handlePostMessage(req, res)
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
res.writeHead(404).end('Not Found')
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
await new Promise<void>((resolve) => {
|
|
104
|
-
httpServer!.listen(port, () => {
|
|
105
|
-
console.error(`codewiki-mcp SSE server listening on http://localhost:${port}${endpoint}`)
|
|
106
|
-
resolve()
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
return
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export async function stopServer(): Promise<void> {
|
|
114
|
-
if (httpServer) {
|
|
115
|
-
await new Promise<void>((resolve, reject) => {
|
|
116
|
-
httpServer!.close((err) => (err ? reject(err) : resolve()))
|
|
117
|
-
})
|
|
118
|
-
httpServer = undefined
|
|
119
|
-
}
|
|
120
|
-
}
|