@foundation0/git 1.0.0 → 1.2.1
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/{packages/git/README.md → README.md} +14 -5
- package/{packages/git/mcp → mcp}/README.md +3 -1
- package/{packages/git/mcp → mcp}/cli.mjs +0 -0
- package/package.json +43 -13
- package/{packages/git/src → src}/git-service-api.ts +3 -0
- package/{packages/git/src → src}/index.ts +8 -0
- package/src/label-management.ts +587 -0
- package/src/platform/config.ts +60 -0
- package/.codex.example/config.toml +0 -10
- package/.env.example +0 -10
- package/packages/fs/README.md +0 -47
- package/packages/fs/node_modules/.bin/f0-git-mcp +0 -21
- package/packages/fs/node_modules/.bin/f0-git-mcp-server +0 -21
- package/packages/fs/node_modules/.bin/f0-git-mcp-server.CMD +0 -12
- package/packages/fs/node_modules/.bin/f0-git-mcp-server.ps1 +0 -41
- package/packages/fs/node_modules/.bin/f0-git-mcp.CMD +0 -12
- package/packages/fs/node_modules/.bin/f0-git-mcp.ps1 +0 -41
- package/packages/fs/node_modules/.bin/tsc +0 -21
- package/packages/fs/node_modules/.bin/tsc.CMD +0 -12
- package/packages/fs/node_modules/.bin/tsc.ps1 +0 -41
- package/packages/fs/node_modules/.bin/tsserver +0 -21
- package/packages/fs/node_modules/.bin/tsserver.CMD +0 -12
- package/packages/fs/node_modules/.bin/tsserver.ps1 +0 -41
- package/packages/fs/node_modules/.bin/vite +0 -21
- package/packages/fs/node_modules/.bin/vite.CMD +0 -12
- package/packages/fs/node_modules/.bin/vite.ps1 +0 -41
- package/packages/fs/node_modules/.bin/vitest +0 -21
- package/packages/fs/node_modules/.bin/vitest.CMD +0 -12
- package/packages/fs/node_modules/.bin/vitest.ps1 +0 -41
- package/packages/fs/package.json +0 -28
- package/packages/fs/src/cli.ts +0 -74
- package/packages/fs/src/git-fs.ts +0 -705
- package/packages/fs/src/index.ts +0 -33
- package/packages/fs/src/mount.ts +0 -297
- package/packages/fs/tsconfig.json +0 -7
- package/packages/git/mcp/tests/e2e/git-mcp-e2e.spec.ts +0 -157
- package/packages/git/mcp/tests/e2e/server.fixture.ts +0 -109
- package/packages/git/node_modules/.bin/tsc +0 -21
- package/packages/git/node_modules/.bin/tsc.CMD +0 -12
- package/packages/git/node_modules/.bin/tsc.ps1 +0 -41
- package/packages/git/node_modules/.bin/tsserver +0 -21
- package/packages/git/node_modules/.bin/tsserver.CMD +0 -12
- package/packages/git/node_modules/.bin/tsserver.ps1 +0 -41
- package/packages/git/node_modules/.bin/vite +0 -21
- package/packages/git/node_modules/.bin/vite.CMD +0 -12
- package/packages/git/node_modules/.bin/vite.ps1 +0 -41
- package/packages/git/node_modules/.bin/vitest +0 -21
- package/packages/git/node_modules/.bin/vitest.CMD +0 -12
- package/packages/git/node_modules/.bin/vitest.ps1 +0 -41
- package/packages/git/node_modules/.vite/vitest/results.json +0 -1
- package/packages/git/package.json +0 -60
- package/packages/git/scripts/create-issue.mjs +0 -93
- package/packages/git/scripts/extract-git-spec.mjs +0 -234
- package/packages/git/scripts/fetch-gitea-swagger.mjs +0 -22
- package/packages/git/src/platform/config.ts +0 -140
- package/packages/git/tests/api.spec.ts +0 -55
- package/packages/git/tests/e2e/git-service-feature-e2e.spec.ts +0 -232
- package/packages/git/tests/git-service-api-object.spec.ts +0 -97
- package/packages/git/tests/git-service-feature-matrix.spec.ts +0 -182
- package/packages/git/tests/issue-dependencies.spec.ts +0 -81
- package/packages/git/tsconfig.json +0 -7
- package/packages/git/vitest.config.ts +0 -7
- package/packages/utils/package.json +0 -9
- package/packages/utils/src/awk.ts +0 -6
- package/packages/utils/src/cat.ts +0 -6
- package/packages/utils/src/cd.ts +0 -6
- package/packages/utils/src/chgrp.ts +0 -6
- package/packages/utils/src/chmod.ts +0 -6
- package/packages/utils/src/chown.ts +0 -6
- package/packages/utils/src/cp.ts +0 -6
- package/packages/utils/src/curl.ts +0 -6
- package/packages/utils/src/cut.ts +0 -6
- package/packages/utils/src/date.ts +0 -6
- package/packages/utils/src/echo.ts +0 -6
- package/packages/utils/src/find.ts +0 -6
- package/packages/utils/src/grep.ts +0 -6
- package/packages/utils/src/gunzip.ts +0 -6
- package/packages/utils/src/gzip.ts +0 -6
- package/packages/utils/src/head.ts +0 -6
- package/packages/utils/src/hostname.ts +0 -6
- package/packages/utils/src/index.ts +0 -37
- package/packages/utils/src/ls.ts +0 -6
- package/packages/utils/src/mkdir.ts +0 -6
- package/packages/utils/src/mv.ts +0 -6
- package/packages/utils/src/ping.ts +0 -6
- package/packages/utils/src/pwd.ts +0 -6
- package/packages/utils/src/rm.ts +0 -6
- package/packages/utils/src/rmdir.ts +0 -6
- package/packages/utils/src/sed.ts +0 -6
- package/packages/utils/src/sort.ts +0 -6
- package/packages/utils/src/tail.ts +0 -6
- package/packages/utils/src/tar.ts +0 -6
- package/packages/utils/src/touch.ts +0 -6
- package/packages/utils/src/tr.ts +0 -6
- package/packages/utils/src/uname.ts +0 -6
- package/packages/utils/src/uniq.ts +0 -6
- package/packages/utils/src/unzip.ts +0 -6
- package/packages/utils/src/util.ts +0 -4
- package/packages/utils/src/wc.ts +0 -6
- package/packages/utils/src/wget.ts +0 -6
- package/packages/utils/src/whoami.ts +0 -6
- package/packages/utils/src/zip.ts +0 -6
- package/pnpm-workspace.yaml +0 -2
- package/tsconfig.base.json +0 -12
- /package/{packages/git/gitea-swagger.json → gitea-swagger.json} +0 -0
- /package/{packages/git/mcp → mcp}/src/cli.ts +0 -0
- /package/{packages/git/mcp → mcp}/src/client.ts +0 -0
- /package/{packages/git/mcp → mcp}/src/index.ts +0 -0
- /package/{packages/git/mcp → mcp}/src/server.ts +0 -0
- /package/{packages/git/src → src}/api.ts +0 -0
- /package/{packages/git/src → src}/git-service-feature-spec.generated.ts +0 -0
- /package/{packages/git/src → src}/issue-dependencies.ts +0 -0
- /package/{packages/git/src → src}/platform/gitea-adapter.ts +0 -0
- /package/{packages/git/src → src}/platform/gitea-rules.ts +0 -0
- /package/{packages/git/src → src}/platform/index.ts +0 -0
- /package/{packages/git/src → src}/repository.ts +0 -0
- /package/{packages/git/src → src}/spec-mock.ts +0 -0
package/packages/fs/src/index.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export { createGitFs, defaultGitFs, promises } from './git-fs'
|
|
2
|
-
export {
|
|
3
|
-
access,
|
|
4
|
-
accessSync,
|
|
5
|
-
appendFile,
|
|
6
|
-
appendFileSync,
|
|
7
|
-
copyFile,
|
|
8
|
-
copyFileSync,
|
|
9
|
-
exists,
|
|
10
|
-
existsSync,
|
|
11
|
-
lstat,
|
|
12
|
-
lstatSync,
|
|
13
|
-
mkdir,
|
|
14
|
-
mkdirSync,
|
|
15
|
-
readFile,
|
|
16
|
-
readFileSync,
|
|
17
|
-
readdir,
|
|
18
|
-
readdirSync,
|
|
19
|
-
rename,
|
|
20
|
-
renameSync,
|
|
21
|
-
rm,
|
|
22
|
-
rmSync,
|
|
23
|
-
rmdir,
|
|
24
|
-
rmdirSync,
|
|
25
|
-
stat,
|
|
26
|
-
statSync,
|
|
27
|
-
unlink,
|
|
28
|
-
unlinkSync,
|
|
29
|
-
writeFile,
|
|
30
|
-
writeFileSync,
|
|
31
|
-
} from './git-fs'
|
|
32
|
-
export { mountGitFs, type MountedGitFsHandle } from './mount'
|
|
33
|
-
export type { GitFsOptions, GitFsDirentLike, GitFsStats, GitFsInstance } from './git-fs'
|
package/packages/fs/src/mount.ts
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import { createRequire } from 'node:module'
|
|
2
|
-
import { createGitFs, type GitFsOptions } from './git-fs'
|
|
3
|
-
|
|
4
|
-
type MountedFuse = {
|
|
5
|
-
mount: (path: string, operations: Record<string, unknown>, callback: (error?: Error | null) => void) => void
|
|
6
|
-
unmount: (path: string, callback: (error?: Error | null) => void) => void
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
type MountBackend = 'auto' | 'fuse-bindings' | 'node-fuse-bindings' | 'dokan' | 'winfsp'
|
|
10
|
-
|
|
11
|
-
type GitFsMountOptions = GitFsOptions & {
|
|
12
|
-
mountPoint: string
|
|
13
|
-
backend?: MountBackend
|
|
14
|
-
readOnly?: boolean
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type MountedGitFsHandle = {
|
|
18
|
-
unmount(): Promise<void>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
type FuseErrorMap = Record<string, number>
|
|
22
|
-
|
|
23
|
-
const errnoMap: FuseErrorMap = {
|
|
24
|
-
EACCES: -13,
|
|
25
|
-
EEXIST: -17,
|
|
26
|
-
EIO: -5,
|
|
27
|
-
EISDIR: -21,
|
|
28
|
-
ENOENT: -2,
|
|
29
|
-
ENOTEMPTY: -39,
|
|
30
|
-
ENOTDIR: -20,
|
|
31
|
-
ENOSYS: -38,
|
|
32
|
-
EROFS: -30,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const toFuseErrno = (error: unknown): number => {
|
|
36
|
-
const code = (error as { code?: unknown })?.code
|
|
37
|
-
if (typeof code === 'string' && code in errnoMap) {
|
|
38
|
-
return errnoMap[code]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return -5
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const toFileData = (value: unknown): Buffer => {
|
|
45
|
-
if (Buffer.isBuffer(value)) {
|
|
46
|
-
return value
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (typeof value === 'string') {
|
|
50
|
-
return Buffer.from(value)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return Buffer.from([])
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const toFuseNames = (entries: (string | { name: string })[]): string[] =>
|
|
57
|
-
entries.map((entry) => (typeof entry === 'string' ? entry : entry.name))
|
|
58
|
-
|
|
59
|
-
const getBackendOrder = (backend?: MountBackend): string[] => {
|
|
60
|
-
if (backend === 'dokan') {
|
|
61
|
-
return ['node-fuse-bindings']
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (backend === 'winfsp') {
|
|
65
|
-
return ['node-fuse-bindings', 'fuse-bindings']
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (backend === 'fuse-bindings' || backend === 'node-fuse-bindings') {
|
|
69
|
-
return [backend]
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (process.platform === 'win32') {
|
|
73
|
-
return ['node-fuse-bindings', 'fuse-bindings']
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return ['fuse-bindings']
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const getBackendName = (backend?: MountBackend): string => {
|
|
80
|
-
if (!backend || backend === 'auto') {
|
|
81
|
-
return process.platform === 'win32' ? 'dokan-or-fuse' : 'fuse'
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return backend
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const loadFuseBindings = (backend?: MountBackend): MountedFuse => {
|
|
88
|
-
const require = createRequire(import.meta.url)
|
|
89
|
-
const attemptOrder = getBackendOrder(backend)
|
|
90
|
-
const isBunRuntime = Boolean(process.versions.bun)
|
|
91
|
-
const errors: { name: string; error: unknown }[] = []
|
|
92
|
-
|
|
93
|
-
for (const name of attemptOrder) {
|
|
94
|
-
try {
|
|
95
|
-
return require(name) as MountedFuse
|
|
96
|
-
} catch (error) {
|
|
97
|
-
errors.push({ name, error })
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const winfspHint = process.platform === 'win32'
|
|
102
|
-
? 'For Windows, install Dokany + node-fuse-bindings (backend=dokan) or try installing fuse-bindings from source.'
|
|
103
|
-
: ''
|
|
104
|
-
|
|
105
|
-
const bunHint = isBunRuntime && process.platform === 'win32'
|
|
106
|
-
? 'Bun uses a separate native ABI (24.x in this environment) and these FUSE packages are not loading successfully under Bun.'
|
|
107
|
-
+ '\nUse Node.js runtime instead (for example via a TS runner) or WSL.'
|
|
108
|
-
: ''
|
|
109
|
-
|
|
110
|
-
const details = errors
|
|
111
|
-
.map(({ name, error }) => {
|
|
112
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
113
|
-
return `- ${name}: ${message}`
|
|
114
|
-
})
|
|
115
|
-
.join('\n')
|
|
116
|
-
|
|
117
|
-
throw new Error(
|
|
118
|
-
`mount backend ${getBackendName(backend)} could not be loaded from ${attemptOrder.join(', ')}.\n`
|
|
119
|
-
+ `Missing dependency or failed native binding installation.\n${details}\n${winfspHint}\n${bunHint}`
|
|
120
|
-
.trim(),
|
|
121
|
-
)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const runFuseOp = async <T>(task: () => Promise<T>, cb: (code: number, value?: T) => void): Promise<void> => {
|
|
125
|
-
try {
|
|
126
|
-
const value = await task()
|
|
127
|
-
cb(0, value)
|
|
128
|
-
} catch (error) {
|
|
129
|
-
cb(toFuseErrno(error))
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return Promise.resolve()
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export { type MountedGitFsHandle }
|
|
136
|
-
|
|
137
|
-
export const mountGitFs = async (options: GitFsMountOptions): Promise<MountedGitFsHandle> => {
|
|
138
|
-
const {
|
|
139
|
-
mountPoint,
|
|
140
|
-
readOnly = false,
|
|
141
|
-
backend,
|
|
142
|
-
...fsOptions
|
|
143
|
-
} = options
|
|
144
|
-
const fs = createGitFs(fsOptions)
|
|
145
|
-
const fuse = loadFuseBindings(backend)
|
|
146
|
-
|
|
147
|
-
const readonlyError = () => toFuseErrno({ code: 'EROFS' })
|
|
148
|
-
const operations: Record<string, unknown> = {
|
|
149
|
-
readdir: (path: string, cb: (code: number, names?: string[]) => void) => {
|
|
150
|
-
void runFuseOp(async () => {
|
|
151
|
-
const entries = await fs.readdir(path)
|
|
152
|
-
return ['.', '..', ...toFuseNames(entries)]
|
|
153
|
-
}, cb)
|
|
154
|
-
},
|
|
155
|
-
|
|
156
|
-
getattr: (path: string, cb: (code: number, stats?: unknown) => void) => {
|
|
157
|
-
void runFuseOp(async () => {
|
|
158
|
-
const stats = await fs.stat(path)
|
|
159
|
-
return {
|
|
160
|
-
mtime: stats.mtime,
|
|
161
|
-
atime: stats.atime,
|
|
162
|
-
ctime: stats.ctime,
|
|
163
|
-
nlink: stats.isDirectory() ? 2 : 1,
|
|
164
|
-
size: stats.size,
|
|
165
|
-
mode: stats.isDirectory() ? 0o040755 : 0o100644,
|
|
166
|
-
uid: 0,
|
|
167
|
-
gid: 0,
|
|
168
|
-
}
|
|
169
|
-
}, cb)
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
open: (_path: string, cb: (code: number, fd?: number) => void) => {
|
|
173
|
-
cb(0, 0)
|
|
174
|
-
},
|
|
175
|
-
|
|
176
|
-
release: (_path: string, _fd: number, cb: (code: number) => void) => {
|
|
177
|
-
cb(0)
|
|
178
|
-
},
|
|
179
|
-
|
|
180
|
-
read: (
|
|
181
|
-
path: string,
|
|
182
|
-
_fd: number,
|
|
183
|
-
buffer: Buffer,
|
|
184
|
-
length: number,
|
|
185
|
-
offset: number,
|
|
186
|
-
cb: (code: number, bytesRead?: number) => void,
|
|
187
|
-
) => {
|
|
188
|
-
void runFuseOp(async () => {
|
|
189
|
-
const data = toFileData(await fs.readFile(path))
|
|
190
|
-
const bytesRead = data.slice(offset, offset + length).copy(buffer)
|
|
191
|
-
return bytesRead
|
|
192
|
-
}, cb)
|
|
193
|
-
},
|
|
194
|
-
|
|
195
|
-
write: readOnly
|
|
196
|
-
? (_: string, _fd: number, _buffer: Buffer, _length: number, _offset: number, cb: (code: number, bytes?: number) => void) => {
|
|
197
|
-
cb(readonlyError())
|
|
198
|
-
}
|
|
199
|
-
: (
|
|
200
|
-
path: string,
|
|
201
|
-
_fd: number,
|
|
202
|
-
buffer: Buffer,
|
|
203
|
-
length: number,
|
|
204
|
-
offset: number,
|
|
205
|
-
cb: (code: number, bytes?: number) => void,
|
|
206
|
-
) => {
|
|
207
|
-
void runFuseOp(async () => {
|
|
208
|
-
const current = await fs.readFile(path).catch(() => Buffer.from([]))
|
|
209
|
-
const existing = toFileData(current)
|
|
210
|
-
const incoming = Buffer.from(buffer).slice(0, length)
|
|
211
|
-
const merged = Buffer.alloc(Math.max(existing.length, offset + incoming.length))
|
|
212
|
-
existing.copy(merged)
|
|
213
|
-
incoming.copy(merged, offset)
|
|
214
|
-
await fs.writeFile(path, merged)
|
|
215
|
-
return incoming.length
|
|
216
|
-
}, cb)
|
|
217
|
-
},
|
|
218
|
-
|
|
219
|
-
create: readOnly
|
|
220
|
-
? (_: string, _mode: number, cb: (code: number, fd?: number) => void) => {
|
|
221
|
-
cb(readonlyError())
|
|
222
|
-
}
|
|
223
|
-
: async (path: string, _mode: number, cb: (code: number, fd?: number) => void) => {
|
|
224
|
-
void runFuseOp(async () => {
|
|
225
|
-
await fs.writeFile(path, '')
|
|
226
|
-
return 0
|
|
227
|
-
}, cb)
|
|
228
|
-
},
|
|
229
|
-
|
|
230
|
-
truncate: readOnly
|
|
231
|
-
? (_path: string, _size: number, cb: (code: number) => void) => {
|
|
232
|
-
cb(readonlyError())
|
|
233
|
-
}
|
|
234
|
-
: async (path: string, size: number, cb: (code: number) => void) => {
|
|
235
|
-
void runFuseOp(async () => {
|
|
236
|
-
const bytes = toFileData(await fs.readFile(path))
|
|
237
|
-
await fs.writeFile(path, bytes.slice(0, size))
|
|
238
|
-
}, cb as (code: number, _value?: void) => void)
|
|
239
|
-
},
|
|
240
|
-
|
|
241
|
-
unlink: readOnly
|
|
242
|
-
? (_path: string, cb: (code: number) => void) => {
|
|
243
|
-
cb(readonlyError())
|
|
244
|
-
}
|
|
245
|
-
: async (path: string, cb: (code: number) => void) => {
|
|
246
|
-
void runFuseOp(async () => {
|
|
247
|
-
await fs.unlink(path)
|
|
248
|
-
}, cb as (code: number, _value?: void) => void)
|
|
249
|
-
},
|
|
250
|
-
|
|
251
|
-
rmdir: readOnly
|
|
252
|
-
? (_path: string, cb: (code: number) => void) => {
|
|
253
|
-
cb(readonlyError())
|
|
254
|
-
}
|
|
255
|
-
: async (path: string, cb: (code: number) => void) => {
|
|
256
|
-
void runFuseOp(async () => {
|
|
257
|
-
await fs.rmdir(path)
|
|
258
|
-
}, cb as (code: number, _value?: void) => void)
|
|
259
|
-
},
|
|
260
|
-
|
|
261
|
-
rename: readOnly
|
|
262
|
-
? (_source: string, _destination: string, cb: (code: number) => void) => {
|
|
263
|
-
cb(readonlyError())
|
|
264
|
-
}
|
|
265
|
-
: async (source: string, destination: string, cb: (code: number) => void) => {
|
|
266
|
-
void runFuseOp(async () => {
|
|
267
|
-
await fs.rename(source, destination)
|
|
268
|
-
}, cb as (code: number, _value?: void) => void)
|
|
269
|
-
},
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
await new Promise<void>((resolve, reject) => {
|
|
273
|
-
fuse.mount(mountPoint, operations, (error?: Error | null) => {
|
|
274
|
-
if (error) {
|
|
275
|
-
reject(error)
|
|
276
|
-
return
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
resolve()
|
|
280
|
-
})
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
unmount: () => {
|
|
285
|
-
return new Promise<void>((resolve, reject) => {
|
|
286
|
-
fuse.unmount(mountPoint, (error?: Error | null) => {
|
|
287
|
-
if (error) {
|
|
288
|
-
reject(error)
|
|
289
|
-
return
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
resolve()
|
|
293
|
-
})
|
|
294
|
-
})
|
|
295
|
-
},
|
|
296
|
-
}
|
|
297
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
|
|
4
|
-
import { GitMcpClient } from '../../src'
|
|
5
|
-
|
|
6
|
-
const fixturePath = resolve(process.cwd(), 'tests/e2e/server.fixture.ts')
|
|
7
|
-
|
|
8
|
-
const connectClient = async (args: string[] = []): Promise<GitMcpClient> => {
|
|
9
|
-
const client = new GitMcpClient({
|
|
10
|
-
name: 'mcp-e2e-client',
|
|
11
|
-
version: '1.0.0',
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
await client.connect('bun', ['run', fixturePath, ...args])
|
|
15
|
-
return client
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('mcp e2e server/client', () => {
|
|
19
|
-
let client: GitMcpClient
|
|
20
|
-
|
|
21
|
-
beforeEach(async () => {
|
|
22
|
-
client = await connectClient()
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
afterEach(async () => {
|
|
26
|
-
await client.close()
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('registers issue tools', async () => {
|
|
30
|
-
const tools = await client.listTools()
|
|
31
|
-
|
|
32
|
-
expect(tools).toContain('repo.issue.create')
|
|
33
|
-
expect(tools).toContain('repo.issue.list')
|
|
34
|
-
expect(tools).toContain('repo.contents.create')
|
|
35
|
-
expect(tools).toContain('repo.contents.view')
|
|
36
|
-
expect(tools).toContain('repo.contents.list')
|
|
37
|
-
expect(tools).toContain('repo.contents.update')
|
|
38
|
-
expect(tools).toContain('repo.contents.delete')
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('executes an api tool and returns execution payload', async () => {
|
|
42
|
-
const response = await client.call('repo.issue.create', {
|
|
43
|
-
options: {
|
|
44
|
-
data: {
|
|
45
|
-
title: 'Bug report',
|
|
46
|
-
body: 'Crash repro steps',
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
expect(response.isError).toBe(false)
|
|
52
|
-
expect(response.data).toMatchObject({
|
|
53
|
-
status: 201,
|
|
54
|
-
ok: true,
|
|
55
|
-
body: {
|
|
56
|
-
ok: true,
|
|
57
|
-
action: 'issue.create',
|
|
58
|
-
method: 'POST',
|
|
59
|
-
},
|
|
60
|
-
})
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('executes contents create and list tools', async () => {
|
|
64
|
-
const createResponse = await client.call('repo.contents.create', {
|
|
65
|
-
args: ['README.md'],
|
|
66
|
-
options: {
|
|
67
|
-
data: {
|
|
68
|
-
content: 'cmVhZGVtZSBmaWxl',
|
|
69
|
-
message: 'create file',
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
expect(createResponse.isError).toBe(false)
|
|
75
|
-
expect(createResponse.data).toMatchObject({
|
|
76
|
-
status: 200,
|
|
77
|
-
ok: true,
|
|
78
|
-
body: {
|
|
79
|
-
action: 'repo-mock',
|
|
80
|
-
method: 'POST',
|
|
81
|
-
path: '/repos/example-org/example-repo/contents/README.md',
|
|
82
|
-
},
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
const viewResponse = await client.call('repo.contents.view', {
|
|
86
|
-
args: ['README.md'],
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
expect(viewResponse.isError).toBe(false)
|
|
90
|
-
expect(viewResponse.data).toMatchObject({
|
|
91
|
-
status: 200,
|
|
92
|
-
ok: true,
|
|
93
|
-
body: {
|
|
94
|
-
action: 'repo-mock',
|
|
95
|
-
method: 'GET',
|
|
96
|
-
path: '/repos/example-org/example-repo/contents/README.md',
|
|
97
|
-
},
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
const listResponse = await client.call('repo.contents.list', {
|
|
101
|
-
args: ['docs'],
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
expect(listResponse.isError).toBe(false)
|
|
105
|
-
expect(listResponse.data).toMatchObject({
|
|
106
|
-
status: 200,
|
|
107
|
-
ok: true,
|
|
108
|
-
body: {
|
|
109
|
-
action: 'repo-mock',
|
|
110
|
-
method: 'GET',
|
|
111
|
-
path: '/repos/example-org/example-repo/contents/docs',
|
|
112
|
-
},
|
|
113
|
-
})
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('honors tool name prefix in server configuration', async () => {
|
|
117
|
-
const prefixedClient = await connectClient(['--tools-prefix=git'])
|
|
118
|
-
const tools = await prefixedClient.listTools()
|
|
119
|
-
|
|
120
|
-
await prefixedClient.close()
|
|
121
|
-
|
|
122
|
-
expect(tools).toContain('git.repo.issue.create')
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('registers and executes batch tool', async () => {
|
|
126
|
-
const tools = await client.listTools()
|
|
127
|
-
const response = await client.call('batch', {
|
|
128
|
-
calls: [
|
|
129
|
-
{
|
|
130
|
-
tool: 'repo.issue.create',
|
|
131
|
-
options: {
|
|
132
|
-
data: {
|
|
133
|
-
title: 'Bug report from batch',
|
|
134
|
-
body: 'Crash repro steps',
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
tool: 'repo.contents.list',
|
|
140
|
-
args: ['docs'],
|
|
141
|
-
},
|
|
142
|
-
],
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
expect(tools).toContain('batch')
|
|
146
|
-
expect(response.isError).toBe(false)
|
|
147
|
-
expect(Array.isArray(response.data)).toBe(true)
|
|
148
|
-
expect(response.data).toMatchObject([
|
|
149
|
-
{ index: 0, tool: 'repo.issue.create', isError: false },
|
|
150
|
-
{ index: 1, tool: 'repo.contents.list', isError: false },
|
|
151
|
-
])
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it('throws a transport error for unknown tools', async () => {
|
|
155
|
-
await expect(client.call('repo.issue.invalid')).rejects.toThrow()
|
|
156
|
-
})
|
|
157
|
-
})
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { createGitMcpServer } from '../../src/server'
|
|
2
|
-
|
|
3
|
-
const toolsPrefixArg = process.argv.find((value) => value.startsWith('--tools-prefix='))
|
|
4
|
-
const toolsPrefix = toolsPrefixArg?.slice('--tools-prefix='.length)
|
|
5
|
-
const toolsPrefixNormalized = toolsPrefix && toolsPrefix.trim().length > 0 ? toolsPrefix : undefined
|
|
6
|
-
|
|
7
|
-
type MockResponseBody = Record<string, unknown>
|
|
8
|
-
|
|
9
|
-
const jsonResponse = (body: MockResponseBody, status = 200): Response =>
|
|
10
|
-
new Response(JSON.stringify(body), {
|
|
11
|
-
status,
|
|
12
|
-
headers: {
|
|
13
|
-
'content-type': 'application/json',
|
|
14
|
-
},
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
const parseJsonBody = async (body: BodyInit | null | undefined): Promise<unknown> => {
|
|
18
|
-
if (body === null || body === undefined) {
|
|
19
|
-
return null
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (typeof body === 'string' || body instanceof String) {
|
|
23
|
-
try {
|
|
24
|
-
return JSON.parse(String(body))
|
|
25
|
-
} catch {
|
|
26
|
-
return String(body)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
return await new Response(body).json()
|
|
32
|
-
} catch {
|
|
33
|
-
return String(body)
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Lightweight transport-safe mock for fetch so the MCP fixture never touches the network.
|
|
38
|
-
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
39
|
-
const requestUrl = String(input)
|
|
40
|
-
const method = (init?.method ?? 'GET').toUpperCase()
|
|
41
|
-
const parsedUrl = new URL(requestUrl)
|
|
42
|
-
const body = await parseJsonBody(init?.body)
|
|
43
|
-
|
|
44
|
-
const query: Record<string, string> = Object.fromEntries(parsedUrl.searchParams.entries())
|
|
45
|
-
|
|
46
|
-
if (parsedUrl.pathname.endsWith('/repos/example-org/example-repo/issues')) {
|
|
47
|
-
if (method === 'POST') {
|
|
48
|
-
return jsonResponse(
|
|
49
|
-
{
|
|
50
|
-
ok: true,
|
|
51
|
-
action: 'issue.create',
|
|
52
|
-
method,
|
|
53
|
-
path: parsedUrl.pathname,
|
|
54
|
-
query,
|
|
55
|
-
body,
|
|
56
|
-
},
|
|
57
|
-
201,
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return jsonResponse({
|
|
62
|
-
ok: true,
|
|
63
|
-
action: 'issue.list',
|
|
64
|
-
method,
|
|
65
|
-
path: parsedUrl.pathname,
|
|
66
|
-
query,
|
|
67
|
-
body,
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (parsedUrl.pathname.includes('/api/v1/repos/example-org/example-repo')) {
|
|
72
|
-
return jsonResponse({
|
|
73
|
-
ok: true,
|
|
74
|
-
action: 'repo-mock',
|
|
75
|
-
method,
|
|
76
|
-
path: parsedUrl.pathname,
|
|
77
|
-
query,
|
|
78
|
-
body,
|
|
79
|
-
})
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return jsonResponse({
|
|
83
|
-
ok: true,
|
|
84
|
-
action: 'generic',
|
|
85
|
-
method,
|
|
86
|
-
path: parsedUrl.pathname,
|
|
87
|
-
query,
|
|
88
|
-
body,
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const fixture = createGitMcpServer({
|
|
93
|
-
toolsPrefix: toolsPrefixNormalized,
|
|
94
|
-
config: {
|
|
95
|
-
platform: 'GITEA',
|
|
96
|
-
giteaHost: 'https://gitea.example.com',
|
|
97
|
-
giteaToken: 'e2e-token',
|
|
98
|
-
},
|
|
99
|
-
defaultOwner: 'example-org',
|
|
100
|
-
defaultRepo: 'example-repo',
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
await fixture.run()
|
|
104
|
-
|
|
105
|
-
await new Promise<void>((resolve) => {
|
|
106
|
-
process.stdin.on('close', resolve)
|
|
107
|
-
process.on('SIGINT', resolve)
|
|
108
|
-
process.on('SIGTERM', resolve)
|
|
109
|
-
})
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
-
|
|
4
|
-
case `uname` in
|
|
5
|
-
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
-
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
-
basedir=`cygpath -w "$basedir"`
|
|
8
|
-
fi
|
|
9
|
-
;;
|
|
10
|
-
esac
|
|
11
|
-
|
|
12
|
-
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/mnt/c/Users/marko/Dev/F0/git/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/mnt/c/Users/marko/Dev/F0/git/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/mnt/c/Users/marko/Dev/F0/git/node_modules/.pnpm/typescript@5.9.3/node_modules:/mnt/c/Users/marko/Dev/F0/git/node_modules/.pnpm/node_modules"
|
|
14
|
-
else
|
|
15
|
-
export NODE_PATH="/mnt/c/Users/marko/Dev/F0/git/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/mnt/c/Users/marko/Dev/F0/git/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/mnt/c/Users/marko/Dev/F0/git/node_modules/.pnpm/typescript@5.9.3/node_modules:/mnt/c/Users/marko/Dev/F0/git/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
-
fi
|
|
17
|
-
if [ -x "$basedir/node" ]; then
|
|
18
|
-
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
|
|
19
|
-
else
|
|
20
|
-
exec node "$basedir/../typescript/bin/tsc" "$@"
|
|
21
|
-
fi
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
@SETLOCAL
|
|
2
|
-
@IF NOT DEFINED NODE_PATH (
|
|
3
|
-
@SET "NODE_PATH=C:\Users\marko\Dev\F0\git\node_modules\.pnpm\typescript@5.9.3\node_modules\typescript\bin\node_modules;C:\Users\marko\Dev\F0\git\node_modules\.pnpm\typescript@5.9.3\node_modules\typescript\node_modules;C:\Users\marko\Dev\F0\git\node_modules\.pnpm\typescript@5.9.3\node_modules;C:\Users\marko\Dev\F0\git\node_modules\.pnpm\node_modules"
|
|
4
|
-
) ELSE (
|
|
5
|
-
@SET "NODE_PATH=C:\Users\marko\Dev\F0\git\node_modules\.pnpm\typescript@5.9.3\node_modules\typescript\bin\node_modules;C:\Users\marko\Dev\F0\git\node_modules\.pnpm\typescript@5.9.3\node_modules\typescript\node_modules;C:\Users\marko\Dev\F0\git\node_modules\.pnpm\typescript@5.9.3\node_modules;C:\Users\marko\Dev\F0\git\node_modules\.pnpm\node_modules;%NODE_PATH%"
|
|
6
|
-
)
|
|
7
|
-
@IF EXIST "%~dp0\node.exe" (
|
|
8
|
-
"%~dp0\node.exe" "%~dp0\..\typescript\bin\tsc" %*
|
|
9
|
-
) ELSE (
|
|
10
|
-
@SET PATHEXT=%PATHEXT:;.JS;=;%
|
|
11
|
-
node "%~dp0\..\typescript\bin\tsc" %*
|
|
12
|
-
)
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env pwsh
|
|
2
|
-
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
|
3
|
-
|
|
4
|
-
$exe=""
|
|
5
|
-
$pathsep=":"
|
|
6
|
-
$env_node_path=$env:NODE_PATH
|
|
7
|
-
$new_node_path="C:\Users\marko\Dev\F0\git\node_modules\.pnpm\typescript@5.9.3\node_modules\typescript\bin\node_modules;C:\Users\marko\Dev\F0\git\node_modules\.pnpm\typescript@5.9.3\node_modules\typescript\node_modules;C:\Users\marko\Dev\F0\git\node_modules\.pnpm\typescript@5.9.3\node_modules;C:\Users\marko\Dev\F0\git\node_modules\.pnpm\node_modules"
|
|
8
|
-
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
|
9
|
-
# Fix case when both the Windows and Linux builds of Node
|
|
10
|
-
# are installed in the same directory
|
|
11
|
-
$exe=".exe"
|
|
12
|
-
$pathsep=";"
|
|
13
|
-
} else {
|
|
14
|
-
$new_node_path="/mnt/c/Users/marko/Dev/F0/git/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/mnt/c/Users/marko/Dev/F0/git/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/mnt/c/Users/marko/Dev/F0/git/node_modules/.pnpm/typescript@5.9.3/node_modules:/mnt/c/Users/marko/Dev/F0/git/node_modules/.pnpm/node_modules"
|
|
15
|
-
}
|
|
16
|
-
if ([string]::IsNullOrEmpty($env_node_path)) {
|
|
17
|
-
$env:NODE_PATH=$new_node_path
|
|
18
|
-
} else {
|
|
19
|
-
$env:NODE_PATH="$new_node_path$pathsep$env_node_path"
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
$ret=0
|
|
23
|
-
if (Test-Path "$basedir/node$exe") {
|
|
24
|
-
# Support pipeline input
|
|
25
|
-
if ($MyInvocation.ExpectingInput) {
|
|
26
|
-
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
|
|
27
|
-
} else {
|
|
28
|
-
& "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
|
|
29
|
-
}
|
|
30
|
-
$ret=$LASTEXITCODE
|
|
31
|
-
} else {
|
|
32
|
-
# Support pipeline input
|
|
33
|
-
if ($MyInvocation.ExpectingInput) {
|
|
34
|
-
$input | & "node$exe" "$basedir/../typescript/bin/tsc" $args
|
|
35
|
-
} else {
|
|
36
|
-
& "node$exe" "$basedir/../typescript/bin/tsc" $args
|
|
37
|
-
}
|
|
38
|
-
$ret=$LASTEXITCODE
|
|
39
|
-
}
|
|
40
|
-
$env:NODE_PATH=$env_node_path
|
|
41
|
-
exit $ret
|