@remogram/mcp 0.1.0-beta.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,7 @@
1
+ #!/usr/bin/env node
2
+ import { startServer } from '../server.mjs';
3
+
4
+ startServer().catch((err) => {
5
+ console.error(err);
6
+ process.exit(1);
7
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@remogram/mcp",
3
+ "version": "0.1.0-beta.0",
4
+ "description": "Remogram MCP server delegating to CLI",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/attebury/remogram.git",
10
+ "directory": "packages/remogram-mcp"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": [
16
+ "bin/",
17
+ "register-tools.mjs",
18
+ "run-cli.mjs",
19
+ "server.mjs"
20
+ ],
21
+ "bin": {
22
+ "remogram-mcp": "bin/remogram-mcp.js"
23
+ },
24
+ "exports": {
25
+ ".": "./server.mjs",
26
+ "./run-cli": "./run-cli.mjs"
27
+ },
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.17.0",
33
+ "@remogram/cli": "0.1.0-beta.0",
34
+ "zod": "^3.25.76"
35
+ }
36
+ }
@@ -0,0 +1,93 @@
1
+ import { z } from 'zod';
2
+ import { runRemogramCli, packetToMcpContent } from './run-cli.mjs';
3
+
4
+ export function registerTools(server) {
5
+ const tools = [
6
+ {
7
+ name: 'doctor',
8
+ description: 'Read-only provider readiness diagnostics for config, remote trust, auth, capabilities, and checks.',
9
+ inputSchema: z.object({}),
10
+ args: ['doctor'],
11
+ },
12
+ {
13
+ name: 'provider_capabilities',
14
+ description: 'Structured provider capability facts for commands, auth, checks, host binding, pagination, and write support.',
15
+ inputSchema: z.object({}),
16
+ args: ['provider', 'capabilities'],
17
+ },
18
+ {
19
+ name: 'repo_status',
20
+ description: 'Forge repo status facts (auth, capabilities, default branch).',
21
+ inputSchema: z.object({}),
22
+ args: ['repo', 'status'],
23
+ },
24
+ {
25
+ name: 'ref_compare',
26
+ description: 'Compare two refs with exact SHAs and ahead/behind counts.',
27
+ inputSchema: z.object({
28
+ base: z.string().describe('Base ref'),
29
+ head: z.string().describe('Head ref'),
30
+ }),
31
+ args: (input) => ['refs', 'compare', '--base', input.base, '--head', input.head],
32
+ },
33
+ {
34
+ name: 'pr_status',
35
+ description: 'PR metadata and mergeability facts.',
36
+ inputSchema: z.object({
37
+ number: z.number().int().positive(),
38
+ }),
39
+ args: (input) => ['pr', 'view', '--number', String(input.number)],
40
+ },
41
+ {
42
+ name: 'pr_checks',
43
+ description: 'CI/check conclusions for a PR number or git ref.',
44
+ inputSchema: z.object({
45
+ number: z.number().int().positive().optional(),
46
+ ref: z.string().optional(),
47
+ }),
48
+ args: (input) => {
49
+ const a = ['pr', 'checks'];
50
+ if (input.number != null) a.push('--number', String(input.number));
51
+ if (input.ref) a.push('--ref', input.ref);
52
+ return a;
53
+ },
54
+ },
55
+ {
56
+ name: 'merge_plan',
57
+ description: 'Merge readiness facts: mergeability, checks, blockers.',
58
+ inputSchema: z.object({
59
+ number: z.number().int().positive(),
60
+ }),
61
+ args: (input) => ['merge', 'plan', '--number', String(input.number)],
62
+ },
63
+ {
64
+ name: 'sync_plan',
65
+ description: 'Local vs remote sync facts and divergent-remote blockers.',
66
+ inputSchema: z.object({
67
+ remote: z.string().optional(),
68
+ }),
69
+ args: (input) => {
70
+ const a = ['sync', 'plan'];
71
+ if (input.remote) a.push('--remote', input.remote);
72
+ return a;
73
+ },
74
+ },
75
+ ];
76
+
77
+ for (const tool of tools) {
78
+ server.registerTool(
79
+ tool.name,
80
+ {
81
+ description: tool.description,
82
+ inputSchema: tool.inputSchema,
83
+ annotations: { readOnlyHint: true, destructiveHint: false },
84
+ },
85
+ async (input) => {
86
+ const args = typeof tool.args === 'function' ? tool.args(input) : tool.args;
87
+ const result = await runRemogramCli(args);
88
+ const truncated = result.stdoutTruncated || result.stderrTruncated;
89
+ return packetToMcpContent(result.stdout, result.stderr, result.code, truncated);
90
+ },
91
+ );
92
+ }
93
+ }
package/run-cli.mjs ADDED
@@ -0,0 +1,111 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, join } from 'node:path';
5
+ import {
6
+ forgeErrorPacket,
7
+ unknownForgeContext,
8
+ ERROR_CODES,
9
+ forgeError,
10
+ capText,
11
+ sanitizeField,
12
+ } from '@remogram/core';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const require = createRequire(import.meta.url);
16
+ const MAX_OUTPUT_BYTES = 65_536;
17
+
18
+ function safeCliErrorMessage(stderr, stdout) {
19
+ const raw = stderr || stdout || '';
20
+ const capped = capText(raw, MAX_OUTPUT_BYTES);
21
+ const sanitized = sanitizeField(capped.text);
22
+ if (!sanitized) return 'CLI did not return JSON';
23
+ if (/Bearer\s|ghp_|gho_|glpat-|GITLAB_TOKEN|GITEA_TOKEN/i.test(sanitized)) {
24
+ return 'CLI did not return JSON';
25
+ }
26
+ return sanitized;
27
+ }
28
+
29
+ export function remogramCwd() {
30
+ return process.env.REMOGRAM_CWD || process.cwd();
31
+ }
32
+
33
+ export function resolveCliBin() {
34
+ try {
35
+ return require.resolve('@remogram/cli/bin/remogram.js');
36
+ } catch {
37
+ return join(__dirname, '../remogram-cli/bin/remogram.js');
38
+ }
39
+ }
40
+
41
+ function appendCapped(current, chunk, maxBytes) {
42
+ const next = current + chunk;
43
+ const capped = capText(next, maxBytes);
44
+ return { text: capped.text, truncated: capped.truncated };
45
+ }
46
+
47
+ export function runRemogramCli(args) {
48
+ const cliBin = resolveCliBin();
49
+ return new Promise((resolve, reject) => {
50
+ const child = spawn(process.execPath, [cliBin, ...args, '--json'], {
51
+ cwd: remogramCwd(),
52
+ env: { ...process.env, REMOGRAM_CWD: remogramCwd() },
53
+ stdio: ['ignore', 'pipe', 'pipe'],
54
+ });
55
+ let stdout = '';
56
+ let stderr = '';
57
+ let stdoutTruncated = false;
58
+ let stderrTruncated = false;
59
+ child.stdout.on('data', (d) => {
60
+ const chunk = d.toString();
61
+ const capped = appendCapped(stdout, chunk, MAX_OUTPUT_BYTES);
62
+ stdout = capped.text;
63
+ stdoutTruncated = stdoutTruncated || capped.truncated;
64
+ });
65
+ child.stderr.on('data', (d) => {
66
+ const chunk = d.toString();
67
+ const capped = appendCapped(stderr, chunk, MAX_OUTPUT_BYTES);
68
+ stderr = capped.text;
69
+ stderrTruncated = stderrTruncated || capped.truncated;
70
+ });
71
+ child.on('error', reject);
72
+ child.on('close', (code) => {
73
+ resolve({
74
+ code,
75
+ stdout: stdout.trim(),
76
+ stderr: stderr.trim(),
77
+ stdoutTruncated,
78
+ stderrTruncated,
79
+ });
80
+ });
81
+ });
82
+ }
83
+
84
+ export function packetToMcpContent(stdout, stderr, code, truncated = false) {
85
+ let packet;
86
+ let isError = code !== 0 || truncated;
87
+ try {
88
+ packet = JSON.parse(stdout);
89
+ if (packet && packet.ok === false) isError = true;
90
+ } catch {
91
+ isError = true;
92
+ packet = forgeErrorPacket(
93
+ unknownForgeContext(),
94
+ forgeError(
95
+ ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
96
+ safeCliErrorMessage(stderr, stdout),
97
+ ),
98
+ );
99
+ }
100
+ if (truncated) {
101
+ isError = true;
102
+ packet = forgeErrorPacket(
103
+ unknownForgeContext(),
104
+ forgeError(ERROR_CODES.OVERSIZED_RAW_OUTPUT, 'CLI output exceeded MCP byte cap'),
105
+ );
106
+ }
107
+ return {
108
+ isError,
109
+ content: [{ type: 'text', text: JSON.stringify(packet, null, 2) }],
110
+ };
111
+ }
package/server.mjs ADDED
@@ -0,0 +1,10 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { registerTools } from './register-tools.mjs';
4
+
5
+ export async function startServer() {
6
+ const server = new McpServer({ name: 'remogram', version: '0.1.0' });
7
+ registerTools(server);
8
+ const transport = new StdioServerTransport();
9
+ await server.connect(transport);
10
+ }