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.
@@ -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
- }
@@ -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
- }