@alfe.ai/github-mcp 0.3.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/dist/server.d.ts +1 -0
- package/dist/server.js +308 -0
- package/package.json +33 -0
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7
|
+
import { resolveConfig } from "@alfe.ai/config";
|
|
8
|
+
import { AgentApiClient } from "@alfe.ai/agent-api-client";
|
|
9
|
+
import { assertPatternA } from "@alfe.ai/mcp-bundler";
|
|
10
|
+
//#region src/server.ts
|
|
11
|
+
/**
|
|
12
|
+
* GitHub MCP Proxy Server (Pattern A multi-account)
|
|
13
|
+
*
|
|
14
|
+
* A thin proxy that fans out to one official
|
|
15
|
+
* `@modelcontextprotocol/server-github` child process per connected
|
|
16
|
+
* GitHub account. Every credential-touching tool requires the LLM to
|
|
17
|
+
* pass a `login` selector arg (the GitHub username); the proxy strips
|
|
18
|
+
* that arg, dispatches to the right child, and returns the result.
|
|
19
|
+
*
|
|
20
|
+
* Pattern A locked in PR 7-deferred slice 2 of channels-and-credential-
|
|
21
|
+
* driven-integrations. See `packages/mcp-bundler/DEVELOPING.md` for the
|
|
22
|
+
* contract; `services/connect/DEVELOPING.md` for the provider table.
|
|
23
|
+
*
|
|
24
|
+
* Architecture:
|
|
25
|
+
* OpenClaw ←(stdio)→ this proxy ←(stdio fan-out)→ N × server-github
|
|
26
|
+
*
|
|
27
|
+
* Token model:
|
|
28
|
+
* GitHub OAuth App tokens have no expiry (`tokenLifecycle: "no_expiry"`
|
|
29
|
+
* in services/connect's GitHub provider). There is intentionally no
|
|
30
|
+
* token-refresh path — a revoked token surfaces as a 401 on the next
|
|
31
|
+
* call and the LLM can prompt the user to reconnect.
|
|
32
|
+
*
|
|
33
|
+
* Known caveat — child dependency:
|
|
34
|
+
* `@modelcontextprotocol/server-github` is marked deprecated upstream
|
|
35
|
+
* ("Package no longer supported"). It still works at npm install
|
|
36
|
+
* time and is the same dependency the existing GitHub integration
|
|
37
|
+
* manifest invoked directly. Migrating to a native @octokit/rest
|
|
38
|
+
* tool surface is tracked as a v2 follow-up.
|
|
39
|
+
*
|
|
40
|
+
* Uses the low-level Server class (not McpServer) because child tools
|
|
41
|
+
* return JSON Schema objects — McpServer.registerTool requires Zod.
|
|
42
|
+
*/
|
|
43
|
+
const accounts = /* @__PURE__ */ new Map();
|
|
44
|
+
/**
|
|
45
|
+
* Full snapshot of every GitHub connection returned by getGithubAccounts(),
|
|
46
|
+
* including ones we couldn't spawn a child for (e.g. missing access token
|
|
47
|
+
* or spawn failure). `github_list_accounts` returns this so the LLM can
|
|
48
|
+
* surface partial-failure connections instead of silently dropping them.
|
|
49
|
+
*/
|
|
50
|
+
const allAccountsSnapshot = [];
|
|
51
|
+
/** Cached, selector-injected tool list returned to OpenClaw. */
|
|
52
|
+
let cachedTools = [];
|
|
53
|
+
function log(msg) {
|
|
54
|
+
process.stderr.write(`[github-mcp-proxy] ${msg}\n`);
|
|
55
|
+
}
|
|
56
|
+
function resolveAccount(login) {
|
|
57
|
+
if (!login) throw new Error("Missing required login argument. Call github_list_accounts to see the connected GitHub accounts and pass the login you want to target.");
|
|
58
|
+
const acct = accounts.get(login);
|
|
59
|
+
if (!acct) {
|
|
60
|
+
const known = allAccountsSnapshot.find((s) => s.login === login);
|
|
61
|
+
if (known && !known.connected) throw new Error(`login ${login} is connected on this agent but the proxy could not initialise a child server for it (reason: ${known.reason ?? "unknown"}). Ask the user to reconnect this GitHub account from the dashboard.`);
|
|
62
|
+
throw new Error(`Unknown login: ${login}. Call github_list_accounts to see the connected GitHub accounts on this agent.`);
|
|
63
|
+
}
|
|
64
|
+
return acct;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Pin the child server-github version to match `package.json` so a future
|
|
68
|
+
* upstream republish (or removal — the package is deprecated) can't
|
|
69
|
+
* silently rotate the child without our knowledge. `package.json` and
|
|
70
|
+
* this string must stay in sync; the openclaw-github CI runs
|
|
71
|
+
* `pnpm run pin:check` (TODO post-v0.1.0) to catch drift.
|
|
72
|
+
*/
|
|
73
|
+
const CHILD_SERVER_GITHUB_VERSION = "2025.4.8";
|
|
74
|
+
async function spawnChild(accessToken) {
|
|
75
|
+
const transport = new StdioClientTransport({
|
|
76
|
+
command: "npx",
|
|
77
|
+
args: ["-y", `@modelcontextprotocol/server-github@${CHILD_SERVER_GITHUB_VERSION}`],
|
|
78
|
+
env: {
|
|
79
|
+
...process.env,
|
|
80
|
+
GITHUB_PERSONAL_ACCESS_TOKEN: accessToken
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const client = new Client({
|
|
84
|
+
name: "github-mcp-proxy",
|
|
85
|
+
version: "0.1.0"
|
|
86
|
+
});
|
|
87
|
+
await client.connect(transport);
|
|
88
|
+
return client;
|
|
89
|
+
}
|
|
90
|
+
async function killAllChildren() {
|
|
91
|
+
for (const acct of accounts.values()) try {
|
|
92
|
+
await acct.client.close();
|
|
93
|
+
} catch {}
|
|
94
|
+
accounts.clear();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Take a tool descriptor from the child GitHub MCP server and inject a
|
|
98
|
+
* required `login` string property into its inputSchema. The proxy
|
|
99
|
+
* strips this arg before forwarding the call.
|
|
100
|
+
*/
|
|
101
|
+
function injectLoginSelector(tool) {
|
|
102
|
+
const original = tool.inputSchema ?? {};
|
|
103
|
+
const originalProperties = original.properties ?? {};
|
|
104
|
+
const originalRequired = Array.isArray(original.required) ? original.required : [];
|
|
105
|
+
const injectedProperties = {
|
|
106
|
+
...originalProperties,
|
|
107
|
+
login: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: "GitHub username (login) — use the value from github_list_accounts to pick which connected account this call should target. Tool-level `owner` arguments still target a specific repo owner; `login` selects which OAuth identity makes the API call."
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const injectedRequired = originalRequired.includes("login") ? originalRequired : ["login", ...originalRequired];
|
|
113
|
+
return {
|
|
114
|
+
name: tool.name,
|
|
115
|
+
description: tool.description,
|
|
116
|
+
inputSchema: {
|
|
117
|
+
...original,
|
|
118
|
+
type: original.type ?? "object",
|
|
119
|
+
properties: injectedProperties,
|
|
120
|
+
required: injectedRequired
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async function main() {
|
|
125
|
+
const config = resolveConfig();
|
|
126
|
+
const { accounts: connected } = await new AgentApiClient({
|
|
127
|
+
apiKey: config.apiKey,
|
|
128
|
+
apiUrl: config.apiUrl
|
|
129
|
+
}).getGithubAccounts();
|
|
130
|
+
if (connected.length === 0) log("No GitHub accounts connected — proxy will start with github_list_accounts and github_check_connection only");
|
|
131
|
+
for (const acct of connected) {
|
|
132
|
+
if (!acct.accessToken) {
|
|
133
|
+
log(`Skipping account ${acct.login} — no access token`);
|
|
134
|
+
allAccountsSnapshot.push({
|
|
135
|
+
login: acct.login,
|
|
136
|
+
displayName: acct.displayName,
|
|
137
|
+
connectedAt: acct.connectedAt,
|
|
138
|
+
connected: false,
|
|
139
|
+
reason: "missing_access_token"
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (accounts.has(acct.login)) {
|
|
144
|
+
log(`Duplicate login ${acct.login} returned by getGithubAccounts() — keeping the first cached child`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const client = await spawnChild(acct.accessToken);
|
|
149
|
+
accounts.set(acct.login, {
|
|
150
|
+
login: acct.login,
|
|
151
|
+
displayName: acct.displayName,
|
|
152
|
+
accessToken: acct.accessToken,
|
|
153
|
+
client
|
|
154
|
+
});
|
|
155
|
+
allAccountsSnapshot.push({
|
|
156
|
+
login: acct.login,
|
|
157
|
+
displayName: acct.displayName,
|
|
158
|
+
connectedAt: acct.connectedAt,
|
|
159
|
+
connected: true
|
|
160
|
+
});
|
|
161
|
+
log(`Spawned child server for account ${acct.login} (${acct.displayName ?? "unnamed"})`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
164
|
+
log(`Failed to spawn child for account ${acct.login}: ${message}`);
|
|
165
|
+
allAccountsSnapshot.push({
|
|
166
|
+
login: acct.login,
|
|
167
|
+
displayName: acct.displayName,
|
|
168
|
+
connectedAt: acct.connectedAt,
|
|
169
|
+
connected: false,
|
|
170
|
+
reason: `spawn_failed: ${message}`
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const firstAccount = accounts.values().next().value;
|
|
175
|
+
if (firstAccount) {
|
|
176
|
+
const { tools } = await firstAccount.client.listTools();
|
|
177
|
+
cachedTools = tools.map(injectLoginSelector);
|
|
178
|
+
log(`Child MCP server provides ${String(cachedTools.length)} tools (selector injected)`);
|
|
179
|
+
} else {
|
|
180
|
+
cachedTools = [];
|
|
181
|
+
log("No child server available — only github_list_accounts and github_check_connection will be exposed");
|
|
182
|
+
}
|
|
183
|
+
cachedTools.push({
|
|
184
|
+
name: "github_list_accounts",
|
|
185
|
+
description: "List the GitHub accounts the agent has connected. Returns one entry per OAuth connection — use the returned login values as the `login` selector arg on every other GitHub tool.",
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: "object",
|
|
188
|
+
properties: {}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
cachedTools.push({
|
|
192
|
+
name: "github_check_connection",
|
|
193
|
+
description: "Verify the GitHub OAuth connection for a specific connected account is still valid. Use this if GitHub API calls are failing with authentication errors. GitHub OAuth App tokens don't expire, so a failure here means the token was revoked.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: "object",
|
|
196
|
+
properties: { login: {
|
|
197
|
+
type: "string",
|
|
198
|
+
description: "GitHub username (login) — use the value from github_list_accounts."
|
|
199
|
+
} },
|
|
200
|
+
required: ["login"]
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
assertPatternA(cachedTools.map((t) => ({
|
|
204
|
+
name: t.name,
|
|
205
|
+
parameters: t.inputSchema
|
|
206
|
+
})), {
|
|
207
|
+
selector: "login",
|
|
208
|
+
exempt: ["github_list_accounts"]
|
|
209
|
+
});
|
|
210
|
+
const proxy = new Server({
|
|
211
|
+
name: "github-mcp-proxy",
|
|
212
|
+
version: "0.1.0"
|
|
213
|
+
}, { capabilities: { tools: {} } });
|
|
214
|
+
proxy.setRequestHandler(ListToolsRequestSchema, () => ({ tools: cachedTools }));
|
|
215
|
+
proxy.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
216
|
+
const { name, arguments: args } = request.params;
|
|
217
|
+
const argMap = args ?? {};
|
|
218
|
+
if (name === "github_list_accounts") return { content: [{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: JSON.stringify({ accounts: allAccountsSnapshot }, null, 2)
|
|
221
|
+
}] };
|
|
222
|
+
if (name === "github_check_connection") try {
|
|
223
|
+
const acct = resolveAccount(typeof argMap.login === "string" ? argMap.login : void 0);
|
|
224
|
+
const response = await fetch("https://api.github.com/user", { headers: {
|
|
225
|
+
Authorization: `Bearer ${acct.accessToken}`,
|
|
226
|
+
Accept: "application/vnd.github+json",
|
|
227
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
228
|
+
"User-Agent": "alfe-openclaw-github"
|
|
229
|
+
} });
|
|
230
|
+
if (!response.ok) return {
|
|
231
|
+
content: [{
|
|
232
|
+
type: "text",
|
|
233
|
+
text: JSON.stringify({
|
|
234
|
+
login: acct.login,
|
|
235
|
+
connected: false,
|
|
236
|
+
status: response.status,
|
|
237
|
+
error: "Token may have been revoked. Ask the user to reconnect this GitHub account from the dashboard."
|
|
238
|
+
})
|
|
239
|
+
}],
|
|
240
|
+
isError: true
|
|
241
|
+
};
|
|
242
|
+
const user = await response.json();
|
|
243
|
+
return { content: [{
|
|
244
|
+
type: "text",
|
|
245
|
+
text: JSON.stringify({
|
|
246
|
+
login: acct.login,
|
|
247
|
+
connected: true,
|
|
248
|
+
user
|
|
249
|
+
})
|
|
250
|
+
}] };
|
|
251
|
+
} catch (err) {
|
|
252
|
+
return {
|
|
253
|
+
content: [{
|
|
254
|
+
type: "text",
|
|
255
|
+
text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
256
|
+
}],
|
|
257
|
+
isError: true
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
let acct;
|
|
261
|
+
try {
|
|
262
|
+
acct = resolveAccount(typeof argMap.login === "string" ? argMap.login : void 0);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
return {
|
|
265
|
+
content: [{
|
|
266
|
+
type: "text",
|
|
267
|
+
text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
268
|
+
}],
|
|
269
|
+
isError: true
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const { login: _ignored, ...forwarded } = argMap;
|
|
273
|
+
try {
|
|
274
|
+
return await acct.client.callTool({
|
|
275
|
+
name,
|
|
276
|
+
arguments: forwarded
|
|
277
|
+
});
|
|
278
|
+
} catch (err) {
|
|
279
|
+
return {
|
|
280
|
+
content: [{
|
|
281
|
+
type: "text",
|
|
282
|
+
text: JSON.stringify({
|
|
283
|
+
login: acct.login,
|
|
284
|
+
tool: name,
|
|
285
|
+
error: err instanceof Error ? err.message : String(err)
|
|
286
|
+
})
|
|
287
|
+
}],
|
|
288
|
+
isError: true
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
const transport = new StdioServerTransport();
|
|
293
|
+
await proxy.connect(transport);
|
|
294
|
+
log(`Proxy running with ${String(accounts.size)} connected account(s) and Pattern A selector enforcement`);
|
|
295
|
+
}
|
|
296
|
+
for (const signal of ["SIGTERM", "SIGINT"]) process.on(signal, () => {
|
|
297
|
+
killAllChildren().then(() => {
|
|
298
|
+
process.exit(0);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
main().catch((err) => {
|
|
302
|
+
log(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
303
|
+
killAllChildren().finally(() => {
|
|
304
|
+
process.exit(1);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
//#endregion
|
|
308
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alfe.ai/github-mcp",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "GitHub MCP proxy server — bridges the official @modelcontextprotocol/server-github with Alfe OAuth credentials (Pattern A multi-account)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"github-mcp-proxy": "./dist/server.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/server.d.ts",
|
|
13
|
+
"import": "./dist/server.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": ">=1.24.0",
|
|
21
|
+
"@modelcontextprotocol/server-github": "2025.4.8",
|
|
22
|
+
"@alfe.ai/config": "0.1.0",
|
|
23
|
+
"@alfe.ai/agent-api-client": "0.3.0",
|
|
24
|
+
"@alfe.ai/mcp-bundler": "0.2.1"
|
|
25
|
+
},
|
|
26
|
+
"license": "UNLICENSED",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsdown",
|
|
29
|
+
"dev": "tsdown --watch",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "eslint ."
|
|
32
|
+
}
|
|
33
|
+
}
|