Skip to content

Commit 48d952a

Browse files
committed
feat: add ncu-mcp MCP server for AI agents
Signed-off-by: Daijiro Wachi <daijiro.wachi@gmail.com>
1 parent 4ea22b8 commit 48d952a

9 files changed

Lines changed: 1249 additions & 21 deletions

File tree

.mcp.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mcpServers": {
3+
"ncu-mcp": {
4+
"command": "node",
5+
"args": ["bin/ncu-mcp.js"]
6+
}
7+
}
8+
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ CLI tools for Node.js Core collaborators.
3333
**DEPRECATED**: use [`git node metadata`](./docs/git-node.md#git-node-metadata)
3434
instead.
3535
- [`ncu-ci`](./docs/ncu-ci.md): Parse the results of a Jenkins CI run and display a summary for all the failures.
36+
- [`ncu-mcp`](./docs/ncu-mcp.md): MCP server for using node-core-utils tools in AI agents.
3637

3738
## Usage
3839

bin/ncu-mcp.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { run } from '../lib/mcp_server.js';
2+
3+
await run();

docs/ncu-mcp.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# ncu-mcp
2+
3+
A [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that
4+
exposes node-core-utils tools to AI agents.
5+
6+
## Setup
7+
8+
Add the server to your MCP client configuration. Replace
9+
`/path/to/node-core-utils` with the actual path to this repository.
10+
11+
### Claude Code
12+
13+
This repository includes `.mcp.json`, so no manual configuration is needed
14+
when working inside it. For other projects, add to `.mcp.json`:
15+
16+
```json
17+
{
18+
"mcpServers": {
19+
"ncu-mcp": {
20+
"command": "node",
21+
"args": ["/path/to/node-core-utils/bin/ncu-mcp.js"]
22+
}
23+
}
24+
}
25+
```
26+
27+
### Cursor
28+
29+
Add to `.cursor/mcp.json`:
30+
31+
```json
32+
{
33+
"mcpServers": {
34+
"ncu-mcp": {
35+
"command": "node",
36+
"args": ["/path/to/node-core-utils/bin/ncu-mcp.js"]
37+
}
38+
}
39+
}
40+
```
41+
42+
### VS Code
43+
44+
Add to `.vscode/mcp.json`:
45+
46+
```json
47+
{
48+
"mcpServers": {
49+
"ncu-mcp": {
50+
"command": "node",
51+
"args": ["/path/to/node-core-utils/bin/ncu-mcp.js"]
52+
}
53+
}
54+
}
55+
```
56+
57+
### Codex CLI
58+
59+
Add to `.codex/config.toml` (project) or `~/.codex/config.toml` (global,
60+
macOS/Linux) or `%USERPROFILE%\.codex\config.toml` (global, Windows):
61+
62+
```toml
63+
[mcp_servers.ncu-mcp]
64+
command = "node"
65+
args = ["/path/to/node-core-utils/bin/ncu-mcp.js"]
66+
```
67+
68+
### Claude Desktop
69+
70+
Add to the config file for your platform:
71+
72+
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
73+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
74+
- Linux: `~/.config/Claude/claude_desktop_config.json`
75+
76+
```json
77+
{
78+
"mcpServers": {
79+
"ncu-mcp": {
80+
"command": "node",
81+
"args": ["/path/to/node-core-utils/bin/ncu-mcp.js"]
82+
}
83+
}
84+
}
85+
```
86+
87+
## Prerequisites
88+
89+
GitHub and Jenkins credentials must be configured before using the tools.
90+
See the [README](../README.md) for setup instructions.
91+
92+
## Tools
93+
94+
### `git_node_metadata`
95+
96+
Fetch metadata for a Node.js pull request, including collaborator approvals,
97+
CI status, and review information. Equivalent to
98+
[`git node metadata`](./git-node.md#git-node-metadata).
99+
100+
Input:
101+
102+
| Parameter | Type | Description |
103+
|---|---|---|
104+
| `pr` | string | Pull request URL or number, e.g. `https://github.com/nodejs/node/pull/12345` or `12345` |
105+
106+
### `git_node_land`
107+
108+
Land a Node.js pull request. Equivalent to
109+
[`git node land`](./git-node.md#git-node-land).
110+
111+
Input:
112+
113+
| Parameter | Type | Description |
114+
|---|---|---|
115+
| `pr` | string | Pull request URL or number |
116+
| `yes` | boolean | Skip confirmation prompts (optional) |
117+
118+
### `git_node_status`
119+
120+
Show the status of an in-progress pull request landing. Equivalent to
121+
[`git node status`](./git-node.md).
122+
123+
### `ncu_ci`
124+
125+
Check CI status for a Node.js pull request.
126+
127+
Input:
128+
129+
| Parameter | Type | Description |
130+
|---|---|---|
131+
| `pr` | string | Pull request URL or number |

eslint.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export default [
4242
'promise/always-return': ['error', { ignoreLastCallback: true }],
4343
'n/no-process-exit': 'off',
4444
'n/no-unsupported-features/node-builtins': 'off',
45+
// eslint-import-resolver-node doesn't support wildcard package.json exports (./*),
46+
// so sub-path imports from this package appear unresolved despite being valid.
47+
'import/no-unresolved': ['error', { ignore: ['^@modelcontextprotocol/'] }],
4548
},
4649
settings: {
4750
'import/resolver': {

lib/mcp_server.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { Server } from '@modelcontextprotocol/sdk/server';
2+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3+
import {
4+
CallToolRequestSchema,
5+
ListToolsRequestSchema
6+
} from '@modelcontextprotocol/sdk/types.js';
7+
8+
import { forceRunAsync } from './run.js';
9+
10+
const GIT_NODE = new URL('../bin/git-node.js', import.meta.url).pathname;
11+
12+
function capture(cmd, args) {
13+
return forceRunAsync(cmd, args, {
14+
captureStdout: true,
15+
captureStderr: true,
16+
ignoreFailure: false
17+
});
18+
}
19+
20+
const TOOLS = [
21+
{
22+
name: 'git_node_metadata',
23+
description: 'Fetch metadata for a Node.js pull request ' +
24+
'(collaborators, CI status, reviews, etc.)',
25+
inputSchema: {
26+
type: 'object',
27+
properties: {
28+
pr: {
29+
type: 'string',
30+
description: 'Pull request URL or number, e.g. ' +
31+
'https://github.com/nodejs/node/pull/12345 or 12345'
32+
}
33+
},
34+
required: ['pr']
35+
}
36+
},
37+
{
38+
name: 'git_node_land',
39+
description: 'Land a Node.js pull request (interactive landing process)',
40+
inputSchema: {
41+
type: 'object',
42+
properties: {
43+
pr: {
44+
type: 'string',
45+
description: 'Pull request URL or number'
46+
},
47+
yes: {
48+
type: 'boolean',
49+
description: 'Skip confirmation prompts (default: false)'
50+
}
51+
},
52+
required: ['pr']
53+
}
54+
},
55+
{
56+
name: 'git_node_status',
57+
description: 'Show the status of a Node.js pull request landing',
58+
inputSchema: {
59+
type: 'object',
60+
properties: {},
61+
required: []
62+
}
63+
},
64+
{
65+
name: 'ncu_ci',
66+
description: 'Check CI status for a Node.js pull request or commit',
67+
inputSchema: {
68+
type: 'object',
69+
properties: {
70+
pr: {
71+
type: 'string',
72+
description: 'Pull request URL or number'
73+
}
74+
},
75+
required: ['pr']
76+
}
77+
}
78+
];
79+
80+
export function createServer(captureImpl = capture) {
81+
const server = new Server(
82+
{ name: 'ncu-mcp', version: '1.0.0' },
83+
{ capabilities: { tools: {} } }
84+
);
85+
86+
server.setRequestHandler(ListToolsRequestSchema, async() => ({ tools: TOOLS }));
87+
88+
server.setRequestHandler(CallToolRequestSchema, async(request) => {
89+
const { name, arguments: args } = request.params;
90+
91+
try {
92+
let output;
93+
94+
switch (name) {
95+
case 'git_node_metadata': {
96+
output = await captureImpl('node', [GIT_NODE, 'metadata', String(args.pr)]);
97+
break;
98+
}
99+
case 'git_node_land': {
100+
const landArgs = ['land', String(args.pr)];
101+
if (args.yes) landArgs.push('--yes');
102+
output = await captureImpl('node', [GIT_NODE, ...landArgs]);
103+
break;
104+
}
105+
case 'git_node_status': {
106+
output = await captureImpl('node', [GIT_NODE, 'status']);
107+
break;
108+
}
109+
case 'ncu_ci': {
110+
output = await captureImpl('node', [GIT_NODE, 'metadata', '--ci', String(args.pr)]);
111+
break;
112+
}
113+
default:
114+
return {
115+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
116+
isError: true
117+
};
118+
}
119+
120+
return {
121+
content: [{ type: 'text', text: String(output) }]
122+
};
123+
} catch (err) {
124+
return {
125+
content: [{ type: 'text', text: `Error: ${err.message}` }],
126+
isError: true
127+
};
128+
}
129+
});
130+
131+
return server;
132+
}
133+
134+
export async function run() {
135+
const server = createServer();
136+
const transport = new StdioServerTransport();
137+
await server.connect(transport);
138+
}

0 commit comments

Comments
 (0)