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