@foundation0/git 1.3.0 → 1.3.2

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/mcp/src/cli.ts CHANGED
@@ -1,76 +1,76 @@
1
- import { runGitMcpServer } from './server'
2
-
3
- const getArgValue = (name: string, fallback?: string): string | undefined => {
4
- const exactArg = process.argv.find((arg) => arg.startsWith(`${name}=`))
5
- if (exactArg) {
6
- const [, value] = exactArg.split('=', 2)
7
- return value
8
- }
9
-
10
- const index = process.argv.indexOf(name)
11
- if (index >= 0 && index + 1 < process.argv.length) {
12
- const next = process.argv[index + 1]
13
- if (!next.startsWith('--')) {
14
- return next
15
- }
16
- }
17
-
18
- return fallback
19
- }
20
-
21
- const hasFlag = (name: string): boolean => process.argv.includes(name)
22
-
23
- const isInsecureHttpUrl = (value: string | undefined): boolean => {
24
- if (!value) {
25
- return false
26
- }
27
-
28
- return value.trim().toLowerCase().startsWith('http://')
29
- }
30
-
31
- if (hasFlag('--help') || hasFlag('-h')) {
32
- console.log('Usage: f0-git-mcp [options]')
33
- console.log('Optional flags:')
34
- console.log(' --default-owner <owner>')
35
- console.log(' --default-repo <repo>')
36
- console.log(' --gitea-host <url>')
37
- console.log(' --server-name <name>')
38
- console.log(' --server-version <version>')
39
- console.log(' --tools-prefix <prefix>')
40
- console.log(' --allow-insecure-http')
41
- process.exit(0)
42
- }
43
-
44
- const owner = getArgValue('--default-owner')?.trim()
45
- const repo = getArgValue('--default-repo')?.trim()
46
- const host = getArgValue('--gitea-host', process.env.GITEA_HOST)?.trim()
47
- const token = process.env.GITEA_TOKEN
48
- const serverName = getArgValue('--server-name', 'f0-git-mcp')
49
- const serverVersion = getArgValue('--server-version', '1.0.0')
50
- const toolsPrefix = getArgValue('--tools-prefix') ?? process.env.MCP_TOOLS_PREFIX
51
-
52
- if (!host) {
53
- throw new Error('GITEA_HOST is required. Set process.env.GITEA_HOST or pass --gitea-host.')
54
- }
55
-
56
- if (isInsecureHttpUrl(host) && !hasFlag('--allow-insecure-http')) {
57
- throw new Error(
58
- 'Refusing to send requests to an insecure http:// Gitea host. Use https:// or pass --allow-insecure-http if you really need this for local testing.',
59
- )
60
- }
61
-
62
- void runGitMcpServer({
63
- serverName: serverName ?? undefined,
64
- serverVersion: serverVersion ?? undefined,
65
- config: {
66
- platform: 'GITEA',
67
- giteaHost: host,
68
- giteaToken: token,
69
- },
70
- ...(owner ? { defaultOwner: owner } : {}),
71
- ...(repo ? { defaultRepo: repo } : {}),
72
- toolsPrefix,
73
- }).catch((error) => {
74
- console.error('Failed to start MCP git server', error)
75
- process.exit(1)
76
- })
1
+ import { runGitMcpServer } from './server'
2
+
3
+ const getArgValue = (name: string, fallback?: string): string | undefined => {
4
+ const exactArg = process.argv.find((arg) => arg.startsWith(`${name}=`))
5
+ if (exactArg) {
6
+ const [, value] = exactArg.split('=', 2)
7
+ return value
8
+ }
9
+
10
+ const index = process.argv.indexOf(name)
11
+ if (index >= 0 && index + 1 < process.argv.length) {
12
+ const next = process.argv[index + 1]
13
+ if (!next.startsWith('--')) {
14
+ return next
15
+ }
16
+ }
17
+
18
+ return fallback
19
+ }
20
+
21
+ const hasFlag = (name: string): boolean => process.argv.includes(name)
22
+
23
+ const isInsecureHttpUrl = (value: string | undefined): boolean => {
24
+ if (!value) {
25
+ return false
26
+ }
27
+
28
+ return value.trim().toLowerCase().startsWith('http://')
29
+ }
30
+
31
+ if (hasFlag('--help') || hasFlag('-h')) {
32
+ console.log('Usage: f0-git-mcp [options]')
33
+ console.log('Optional flags:')
34
+ console.log(' --default-owner <owner>')
35
+ console.log(' --default-repo <repo>')
36
+ console.log(' --gitea-host <url>')
37
+ console.log(' --server-name <name>')
38
+ console.log(' --server-version <version>')
39
+ console.log(' --tools-prefix <prefix>')
40
+ console.log(' --allow-insecure-http')
41
+ process.exit(0)
42
+ }
43
+
44
+ const owner = getArgValue('--default-owner')?.trim()
45
+ const repo = getArgValue('--default-repo')?.trim()
46
+ const host = getArgValue('--gitea-host', process.env.GITEA_HOST)?.trim()
47
+ const token = process.env.GITEA_TOKEN
48
+ const serverName = getArgValue('--server-name', 'f0-git-mcp')
49
+ const serverVersion = getArgValue('--server-version', '1.0.0')
50
+ const toolsPrefix = getArgValue('--tools-prefix') ?? process.env.MCP_TOOLS_PREFIX
51
+
52
+ if (!host) {
53
+ throw new Error('GITEA_HOST is required. Set process.env.GITEA_HOST or pass --gitea-host.')
54
+ }
55
+
56
+ if (isInsecureHttpUrl(host) && !hasFlag('--allow-insecure-http')) {
57
+ throw new Error(
58
+ 'Refusing to send requests to an insecure http:// Gitea host. Use https:// or pass --allow-insecure-http if you really need this for local testing.',
59
+ )
60
+ }
61
+
62
+ void runGitMcpServer({
63
+ serverName: serverName ?? undefined,
64
+ serverVersion: serverVersion ?? undefined,
65
+ config: {
66
+ platform: 'GITEA',
67
+ giteaHost: host,
68
+ giteaToken: token,
69
+ },
70
+ ...(owner ? { defaultOwner: owner } : {}),
71
+ ...(repo ? { defaultRepo: repo } : {}),
72
+ toolsPrefix,
73
+ }).catch((error) => {
74
+ console.error('Failed to start MCP git server', error)
75
+ process.exit(1)
76
+ })
package/mcp/src/client.ts CHANGED
@@ -1,147 +1,156 @@
1
- import { Client } from '@modelcontextprotocol/sdk/client/index.js'
2
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
3
- import { CallToolResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
4
-
5
- type ToolResultText = {
6
- type: 'text'
7
- text: string
8
- }
9
-
10
- type ToolResult = {
11
- isError?: boolean
12
- content?: ToolResultText[]
13
- }
14
-
15
- const isRecord = (value: unknown): value is Record<string, unknown> =>
16
- typeof value === 'object' && value !== null && !Array.isArray(value)
17
-
18
- const toText = (result: ToolResult | null): string => {
19
- if (!result || !result.content || result.content.length === 0) {
20
- return ''
21
- }
22
-
23
- return result.content
24
- .map((entry) => (entry.type === 'text' ? entry.text : ''))
25
- .filter((entry) => entry.length > 0)
26
- .join('\n')
27
- }
28
-
29
- const tryParseResult = (text: string): { parsed?: unknown; text: string } => {
30
- if (!text) {
31
- return { text }
32
- }
33
-
34
- try {
35
- return {
36
- text,
37
- parsed: JSON.parse(text),
38
- }
39
- } catch {
40
- return { text }
41
- }
42
- }
43
-
44
- export interface GitMcpClientOptions {
45
- name?: string
46
- version?: string
47
- }
48
-
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
2
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
3
+ import { CallToolResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
4
+
5
+ type ToolResultText = {
6
+ type: 'text'
7
+ text: string
8
+ }
9
+
10
+ type ToolResult = {
11
+ isError?: boolean
12
+ content?: ToolResultText[]
13
+ }
14
+
15
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
16
+ typeof value === 'object' && value !== null && !Array.isArray(value)
17
+
18
+ const toText = (result: ToolResult | null): string => {
19
+ if (!result || !result.content || result.content.length === 0) {
20
+ return ''
21
+ }
22
+
23
+ return result.content
24
+ .map((entry) => (entry.type === 'text' ? entry.text : ''))
25
+ .filter((entry) => entry.length > 0)
26
+ .join('\n')
27
+ }
28
+
29
+ const tryParseResult = (text: string): { parsed?: unknown; text: string } => {
30
+ if (!text) {
31
+ return { text }
32
+ }
33
+
34
+ try {
35
+ return {
36
+ text,
37
+ parsed: JSON.parse(text),
38
+ }
39
+ } catch {
40
+ return { text }
41
+ }
42
+ }
43
+
44
+ export interface GitMcpClientOptions {
45
+ name?: string
46
+ version?: string
47
+ }
48
+
49
49
  export interface GitMcpCallArgs {
50
50
  args?: unknown[]
51
51
  options?: Record<string, unknown>
52
+ format?: 'terse' | 'debug'
53
+ fields?: string[] | string
54
+ validateOnly?: boolean
55
+ full?: boolean
52
56
  [key: string]: unknown
53
57
  }
54
-
55
- export type GitMcpCallResponse = {
56
- isError: boolean
57
- text: string
58
- data: unknown
59
- }
60
-
61
- export class GitMcpClient {
62
- private readonly client: Client
63
- private transport: StdioClientTransport | null = null
64
-
65
- public constructor(options: GitMcpClientOptions = {}) {
66
- this.client = new Client(
67
- {
68
- name: options.name ?? 'git-mcp-client',
69
- version: options.version ?? '1.0.0',
70
- },
71
- {
72
- capabilities: {},
73
- },
74
- )
75
- }
76
-
77
- public async connect(command: string, args: string[] = []): Promise<void> {
78
- if (this.transport) {
79
- throw new Error('Client transport is already connected')
80
- }
81
-
82
- this.transport = new StdioClientTransport({
83
- command,
84
- args,
85
- })
86
-
87
- await this.client.connect(this.transport)
88
- }
89
-
90
- public async close(): Promise<void> {
91
- if (this.transport) {
92
- await this.client.close()
93
- this.transport = null
94
- }
95
- }
96
-
97
- public async listTools(): Promise<string[]> {
98
- const response = await this.client.request(
99
- {
100
- method: 'tools/list',
101
- },
102
- ListToolsResultSchema,
103
- )
104
-
105
- return response.tools.map((tool) => tool.name)
106
- }
107
-
108
- public async call(toolName: string, args: GitMcpCallArgs = {}): Promise<GitMcpCallResponse> {
109
- const response = await this.client.request(
110
- {
111
- method: 'tools/call',
112
- params: {
113
- name: toolName,
114
- arguments: args,
115
- },
116
- },
117
- CallToolResultSchema,
118
- )
119
-
120
- const text = toText(response as ToolResult)
121
- const parsed = tryParseResult(text)
122
-
123
- // Back-compat: if a server returns an { ok:false } envelope but forgets to mark MCP isError,
124
- // treat it as an error on the client side as well.
125
- const parsedIsError =
126
- isRecord(parsed.parsed) && typeof parsed.parsed.ok === 'boolean' ? parsed.parsed.ok === false : false
127
-
128
- return {
129
- isError: Boolean(response.isError) || parsedIsError,
130
- text: parsed.text,
131
- data: parsed.parsed,
132
- }
133
- }
134
-
58
+
59
+ export type GitMcpCallResponse = {
60
+ isError: boolean
61
+ text: string
62
+ data: unknown
63
+ }
64
+
65
+ export class GitMcpClient {
66
+ private readonly client: Client
67
+ private transport: StdioClientTransport | null = null
68
+
69
+ public constructor(options: GitMcpClientOptions = {}) {
70
+ this.client = new Client(
71
+ {
72
+ name: options.name ?? 'git-mcp-client',
73
+ version: options.version ?? '1.0.0',
74
+ },
75
+ {
76
+ capabilities: {},
77
+ },
78
+ )
79
+ }
80
+
81
+ public async connect(command: string, args: string[] = []): Promise<void> {
82
+ if (this.transport) {
83
+ throw new Error('Client transport is already connected')
84
+ }
85
+
86
+ this.transport = new StdioClientTransport({
87
+ command,
88
+ args,
89
+ })
90
+
91
+ await this.client.connect(this.transport)
92
+ }
93
+
94
+ public async close(): Promise<void> {
95
+ if (this.transport) {
96
+ await this.client.close()
97
+ this.transport = null
98
+ }
99
+ }
100
+
101
+ public async listTools(): Promise<string[]> {
102
+ const response = await this.client.request(
103
+ {
104
+ method: 'tools/list',
105
+ },
106
+ ListToolsResultSchema,
107
+ )
108
+
109
+ return response.tools.map((tool) => tool.name)
110
+ }
111
+
112
+ public async call(toolName: string, args: GitMcpCallArgs = {}): Promise<GitMcpCallResponse> {
113
+ const response = await this.client.request(
114
+ {
115
+ method: 'tools/call',
116
+ params: {
117
+ name: toolName,
118
+ arguments: args,
119
+ },
120
+ },
121
+ CallToolResultSchema,
122
+ )
123
+
124
+ const text = toText(response as ToolResult)
125
+ const parsed = tryParseResult(text)
126
+
127
+ // Back-compat: if a server returns an { ok:false } envelope but forgets to mark MCP isError,
128
+ // treat it as an error on the client side as well.
129
+ const parsedIsError =
130
+ isRecord(parsed.parsed) && typeof parsed.parsed.ok === 'boolean' ? parsed.parsed.ok === false : false
131
+
132
+ return {
133
+ isError: Boolean(response.isError) || parsedIsError,
134
+ text: parsed.text,
135
+ data: parsed.parsed,
136
+ }
137
+ }
138
+
135
139
  public async callByPath(
136
140
  path: string,
137
141
  methodArgs: unknown[] = [],
138
142
  methodOptions: Record<string, unknown> = {},
143
+ controls: Pick<GitMcpCallArgs, 'format' | 'fields' | 'validateOnly' | 'full'> = {},
139
144
  ): Promise<GitMcpCallResponse> {
140
- const args = { args: methodArgs.map((value) => String(value)), options: methodOptions }
145
+ const args = {
146
+ args: methodArgs.map((value) => String(value)),
147
+ options: methodOptions,
148
+ ...controls,
149
+ }
141
150
  return this.call(path, args)
142
151
  }
143
152
  }
144
-
145
- export const createGitMcpClient = (options: GitMcpClientOptions = {}): GitMcpClient => {
146
- return new GitMcpClient(options)
147
- }
153
+
154
+ export const createGitMcpClient = (options: GitMcpClientOptions = {}): GitMcpClient => {
155
+ return new GitMcpClient(options)
156
+ }
package/mcp/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
- export {
2
- createGitMcpServer,
3
- runGitMcpServer,
4
- normalizeToolCallNameForServer,
5
- type GitMcpServerOptions,
6
- } from './server'
7
- export { GitMcpClient, createGitMcpClient, type GitMcpCallArgs, type GitMcpClientOptions, type GitMcpCallResponse } from './client'
1
+ export {
2
+ createGitMcpServer,
3
+ runGitMcpServer,
4
+ normalizeToolCallNameForServer,
5
+ type GitMcpServerOptions,
6
+ } from './server'
7
+ export { GitMcpClient, createGitMcpClient, type GitMcpCallArgs, type GitMcpClientOptions, type GitMcpCallResponse } from './client'