@yinuo-ngm/mcp-server 0.1.1 → 0.1.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/README.md +97 -43
- package/lib/register-tools.js +6 -2
- package/lib/tools/hub-v2/client.d.ts +15 -0
- package/lib/tools/hub-v2/client.js +97 -0
- package/lib/tools/hub-v2/config.d.ts +34 -0
- package/lib/tools/hub-v2/config.js +297 -0
- package/lib/tools/hub-v2/docs.tools.d.ts +2 -0
- package/lib/tools/hub-v2/docs.tools.js +81 -0
- package/lib/tools/hub-v2/errors.d.ts +8 -0
- package/lib/tools/hub-v2/errors.js +27 -0
- package/lib/tools/hub-v2/index.d.ts +2 -0
- package/lib/tools/hub-v2/index.js +17 -0
- package/lib/tools/hub-v2/issues.tools.d.ts +2 -0
- package/lib/tools/hub-v2/issues.tools.js +154 -0
- package/lib/tools/hub-v2/projects.tools.d.ts +2 -0
- package/lib/tools/hub-v2/projects.tools.js +28 -0
- package/lib/tools/hub-v2/rd.tools.d.ts +2 -0
- package/lib/tools/hub-v2/rd.tools.js +202 -0
- package/lib/tools/hub-v2/schemas.d.ts +585 -0
- package/lib/tools/hub-v2/schemas.js +167 -0
- package/lib/tools/hub-v2/upload.tools.d.ts +2 -0
- package/lib/tools/hub-v2/upload.tools.js +150 -0
- package/lib/tools/index.d.ts +2 -0
- package/lib/tools/index.js +2 -0
- package/lib/utils/errors.d.ts +5 -0
- package/lib/utils/errors.js +12 -0
- package/lib/utils/result.d.ts +11 -2
- package/lib/utils/result.js +99 -3
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -6,13 +6,17 @@ This package is an AI Agent adapter layer. It is not a business core, not a Fast
|
|
|
6
6
|
|
|
7
7
|
## Positioning
|
|
8
8
|
|
|
9
|
-
The intended boundary is:
|
|
10
|
-
|
|
11
|
-
```text
|
|
12
|
-
MCP client -> packages/mcp-server -> ToolContext.services -> packages/core
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
The intended boundary is:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
MCP client -> packages/mcp-server -> ToolContext.services -> packages/core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For ng-manager local capabilities, MCP tools should prefer `packages/core`. HTTP routes, Electron IPC, CLI commands, and MCP tools should adapt the same local core services instead of duplicating business logic or calling the local Fastify API.
|
|
16
|
+
|
|
17
|
+
Hub V2 tools are the exception: they may call the Hub V2 Token HTTP API because that API is the integration contract Hub V2 exposes to AI Agents and other external clients. Those tools must keep token handling inside configuration/client layers and must not accept token values as tool arguments.
|
|
18
|
+
|
|
19
|
+
The MCP server must not provide arbitrary shell execution, mutate system environment settings, or remotely execute client-side commands.
|
|
16
20
|
|
|
17
21
|
## Safety
|
|
18
22
|
|
|
@@ -34,16 +38,19 @@ execute blocked
|
|
|
34
38
|
dangerous blocked
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
Write tools are registered only for scoped Hub V2 Personal Token workflows that require explicit tool confirmation when implemented. The server does not implement arbitrary shell execution, task start/stop/restart, git pull/checkout/commit/reset, proxy reload, runtime install/remove, file deletion, system environment mutation, or remote client command execution.
|
|
38
42
|
|
|
39
43
|
## Environment
|
|
40
44
|
|
|
41
45
|
```text
|
|
42
|
-
NGM_DATA_DIR ng-manager data directory. Defaults to ~/.ng-manager.
|
|
43
|
-
NGM_WORKSPACE_ROOT Optional workspace hint. Defaults to process.cwd().
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
NGM_DATA_DIR ng-manager data directory. Defaults to ~/.ng-manager.
|
|
47
|
+
NGM_WORKSPACE_ROOT Optional workspace hint. Defaults to process.cwd().
|
|
48
|
+
NGM_MCP_UPLOAD_ROOT Optional extra root for Hub V2 markdown image uploads.
|
|
49
|
+
NGM_MCP_MAX_UPLOAD_BYTES Max Hub V2 markdown image upload bytes. Defaults to 5242880.
|
|
50
|
+
NGM_MCP_MAX_RESULT_CHARS Max MCP text result characters. Defaults to 120000.
|
|
51
|
+
NGM_MCP_ALLOW_WRITE Enables confirmed write tools. Defaults to false.
|
|
52
|
+
NGM_MCP_ALLOW_EXECUTE Enables execute tools. Defaults to false.
|
|
53
|
+
NGM_MCP_ALLOW_DANGEROUS Enables dangerous tools. Defaults to false.
|
|
47
54
|
```
|
|
48
55
|
|
|
49
56
|
## Commands
|
|
@@ -56,19 +63,25 @@ npm run mcp:build
|
|
|
56
63
|
npm run mcp:start
|
|
57
64
|
```
|
|
58
65
|
|
|
59
|
-
Direct workspace commands:
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
npm run dev -w @yinuo-ngm/mcp-server
|
|
63
|
-
npm run build -w @yinuo-ngm/mcp-server
|
|
64
|
-
npm run start -w @yinuo-ngm/mcp-server
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
Direct workspace commands:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm run dev -w @yinuo-ngm/mcp-server
|
|
70
|
+
npm run build -w @yinuo-ngm/mcp-server
|
|
71
|
+
npm run start -w @yinuo-ngm/mcp-server
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
CLI entrypoint:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
ngm mcp
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The server uses stdio transport only. It does not listen on an HTTP port, and stdout is reserved for the MCP protocol.
|
|
81
|
+
|
|
82
|
+
## MCP Client Configuration
|
|
83
|
+
|
|
84
|
+
Built output:
|
|
72
85
|
|
|
73
86
|
```json
|
|
74
87
|
{
|
|
@@ -109,12 +122,27 @@ Development:
|
|
|
109
122
|
}
|
|
110
123
|
}
|
|
111
124
|
}
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
##
|
|
116
|
-
|
|
117
|
-
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Hub V2 Config
|
|
129
|
+
|
|
130
|
+
Hub V2 tools read token configuration from MCP server configuration only. Tool schemas do not accept token arguments.
|
|
131
|
+
|
|
132
|
+
Configuration priority is:
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
tool args project/projectKey
|
|
136
|
+
HUB_V2_* environment variables
|
|
137
|
+
HUB_V2_CONFIG explicit config path
|
|
138
|
+
~/.ng-manager/agent-connections.json
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Use `~/.ng-manager/agent-connections.json` for persistent local configuration, and `HUB_V2_*` environment variables for temporary overrides, tests, or MCP client injection. Treat `agent-connections.json` as a local secret file: do not commit it, do not pass tokens as tool arguments, and do not print full tokens in logs or Agent replies.
|
|
142
|
+
|
|
143
|
+
## Tools
|
|
144
|
+
|
|
145
|
+
Project:
|
|
118
146
|
|
|
119
147
|
```text
|
|
120
148
|
ngm.project.list
|
|
@@ -145,23 +173,49 @@ ngm.git.diff
|
|
|
145
173
|
|
|
146
174
|
The Git tools are registered in this MVP but return a clear "not implemented in core yet" error through the Git service stub. A future phase should add a read-only Git service to `packages/core` first.
|
|
147
175
|
|
|
148
|
-
Runtime:
|
|
149
|
-
|
|
150
|
-
```text
|
|
151
|
-
ngm.runtime.list
|
|
152
|
-
ngm.runtime.resolveForProject
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Proxy:
|
|
176
|
+
Runtime:
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
ngm.runtime.list
|
|
180
|
+
ngm.runtime.resolveForProject
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Proxy:
|
|
156
184
|
|
|
157
185
|
```text
|
|
158
186
|
ngm.proxy.list
|
|
159
187
|
ngm.proxy.validate
|
|
160
188
|
```
|
|
161
189
|
|
|
162
|
-
In this package, "proxy" means ng-manager's current Nginx/proxy management domain, not the operating system global proxy.
|
|
163
|
-
|
|
164
|
-
|
|
190
|
+
In this package, "proxy" means ng-manager's current Nginx/proxy management domain, not the operating system global proxy.
|
|
191
|
+
|
|
192
|
+
Hub V2:
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
hub_v2_projects_list
|
|
196
|
+
hub_v2_projects_get
|
|
197
|
+
hub_v2_docs_list
|
|
198
|
+
hub_v2_docs_get
|
|
199
|
+
hub_v2_docs_get_by_slug
|
|
200
|
+
hub_v2_issues_list
|
|
201
|
+
hub_v2_issues_get
|
|
202
|
+
hub_v2_issues_create
|
|
203
|
+
hub_v2_issues_comment
|
|
204
|
+
hub_v2_issues_update
|
|
205
|
+
hub_v2_upload_markdown_image
|
|
206
|
+
hub_v2_rd_list
|
|
207
|
+
hub_v2_rd_get
|
|
208
|
+
hub_v2_rd_stage_tasks_list
|
|
209
|
+
hub_v2_rd_create
|
|
210
|
+
hub_v2_rd_advance_stage
|
|
211
|
+
hub_v2_rd_stage_tasks_create
|
|
212
|
+
hub_v2_rd_update_progress
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Hub V2 reads use Project Token configuration and writes use Personal Token configuration. `hub_v2_upload_markdown_image` uploads image files for Markdown bodies and returns a snippet that can be inserted into RD descriptions, RD stage-task descriptions, Issue descriptions, or Issue comments before calling the matching create/comment tool. Prefer `HUB_V2_*` environment variables for temporary overrides and `~/.ng-manager/agent-connections.json` for persistent local configuration.
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
## Result Shape
|
|
165
219
|
|
|
166
220
|
All tools return structured JSON as text content:
|
|
167
221
|
|
package/lib/register-tools.js
CHANGED
|
@@ -15,13 +15,17 @@ function registerTools(server, context) {
|
|
|
15
15
|
inputSchema: tool.inputSchema,
|
|
16
16
|
}, async (args) => {
|
|
17
17
|
try {
|
|
18
|
-
(0, assert_tool_policy_1.assertToolPolicy)(policy, tool.name, tool.riskLevel);
|
|
19
18
|
const parsed = tool.inputSchema.parse(args);
|
|
19
|
+
const confirmed = tool.isConfirmed?.(parsed) ?? true;
|
|
20
|
+
const isAllowedPreview = tool.allowPreviewWhenBlocked === true && !confirmed;
|
|
21
|
+
if (!isAllowedPreview) {
|
|
22
|
+
(0, assert_tool_policy_1.assertToolPolicy)(policy, tool.name, tool.riskLevel);
|
|
23
|
+
}
|
|
20
24
|
const result = await tool.handler(parsed, context);
|
|
21
25
|
return (0, result_1.toMcpTextResult)(result);
|
|
22
26
|
}
|
|
23
27
|
catch (error) {
|
|
24
|
-
return (0, result_1.toMcpTextResult)((0, result_1.fail)(tool.name, (0, errors_1.errorMessage)(error)));
|
|
28
|
+
return (0, result_1.toMcpTextResult)((0, result_1.fail)(tool.name, (0, errors_1.errorMessage)(error), (0, errors_1.errorMetadata)(error)));
|
|
25
29
|
}
|
|
26
30
|
});
|
|
27
31
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HubV2ResolvedContext } from "./config";
|
|
2
|
+
export type HttpMethod = "GET" | "POST" | "PATCH" | "DELETE";
|
|
3
|
+
export declare class HubV2Client {
|
|
4
|
+
private readonly context;
|
|
5
|
+
constructor(context: HubV2ResolvedContext);
|
|
6
|
+
tokenUrl(suffix: string, query?: Record<string, unknown>): string;
|
|
7
|
+
personalUrl(suffix: string, query?: Record<string, unknown>): string;
|
|
8
|
+
request<T = unknown>(method: HttpMethod, url: string, body?: Record<string, unknown>, options?: {
|
|
9
|
+
preserveNull?: boolean;
|
|
10
|
+
}): Promise<T>;
|
|
11
|
+
multipart<T = unknown>(method: "POST", url: string, body: FormData): Promise<T>;
|
|
12
|
+
private queryUrl;
|
|
13
|
+
}
|
|
14
|
+
export declare function compact(values: Record<string, unknown>): Record<string, unknown>;
|
|
15
|
+
export declare function compactUndefined(values: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HubV2Client = void 0;
|
|
4
|
+
exports.compact = compact;
|
|
5
|
+
exports.compactUndefined = compactUndefined;
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
class HubV2Client {
|
|
8
|
+
constructor(context) {
|
|
9
|
+
this.context = context;
|
|
10
|
+
}
|
|
11
|
+
tokenUrl(suffix, query) {
|
|
12
|
+
return this.queryUrl("api/token", suffix, query);
|
|
13
|
+
}
|
|
14
|
+
personalUrl(suffix, query) {
|
|
15
|
+
return this.queryUrl("api/personal", suffix, query);
|
|
16
|
+
}
|
|
17
|
+
async request(method, url, body, options = {}) {
|
|
18
|
+
const headers = { Authorization: `Bearer ${this.context.token}` };
|
|
19
|
+
const init = { method, headers };
|
|
20
|
+
if (body !== undefined) {
|
|
21
|
+
headers["Content-Type"] = "application/json";
|
|
22
|
+
init.body = JSON.stringify(options.preserveNull ? compactUndefined(body) : compact(body));
|
|
23
|
+
}
|
|
24
|
+
const response = await fetch(url, init);
|
|
25
|
+
const text = await response.text();
|
|
26
|
+
const parsed = parseResponseBody(text);
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw (0, errors_1.toHubV2HttpError)(response.status, response.statusText, parsed);
|
|
29
|
+
}
|
|
30
|
+
return (parsed ?? { code: "OK", data: null });
|
|
31
|
+
}
|
|
32
|
+
async multipart(method, url, body) {
|
|
33
|
+
const response = await fetch(url, {
|
|
34
|
+
method,
|
|
35
|
+
headers: { Authorization: `Bearer ${this.context.token}` },
|
|
36
|
+
body,
|
|
37
|
+
});
|
|
38
|
+
const text = await response.text();
|
|
39
|
+
const parsed = parseResponseBody(text);
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw (0, errors_1.toHubV2HttpError)(response.status, response.statusText, parsed);
|
|
42
|
+
}
|
|
43
|
+
return (parsed ?? { code: "OK", data: null });
|
|
44
|
+
}
|
|
45
|
+
queryUrl(prefix, suffix, query) {
|
|
46
|
+
let url = `${this.context.baseUrl}/${prefix}/projects/${encodeURIComponent(this.context.projectKey)}${suffix}`;
|
|
47
|
+
const queryString = toQueryString(query ?? {});
|
|
48
|
+
if (queryString) {
|
|
49
|
+
url += `?${queryString}`;
|
|
50
|
+
}
|
|
51
|
+
return url;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.HubV2Client = HubV2Client;
|
|
55
|
+
function compact(values) {
|
|
56
|
+
const result = {};
|
|
57
|
+
for (const [key, value] of Object.entries(values)) {
|
|
58
|
+
if (value !== undefined && value !== null && !(Array.isArray(value) && value.length === 0)) {
|
|
59
|
+
result[key] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
function compactUndefined(values) {
|
|
65
|
+
const result = {};
|
|
66
|
+
for (const [key, value] of Object.entries(values)) {
|
|
67
|
+
if (value !== undefined && !(Array.isArray(value) && value.length === 0)) {
|
|
68
|
+
result[key] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
function toQueryString(values) {
|
|
74
|
+
const params = new URLSearchParams();
|
|
75
|
+
for (const [key, value] of Object.entries(compact(values))) {
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
for (const item of value) {
|
|
78
|
+
params.append(key, String(item));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
params.set(key, String(value));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return params.toString();
|
|
86
|
+
}
|
|
87
|
+
function parseResponseBody(text) {
|
|
88
|
+
if (!text.trim()) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(text);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return text;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type HubV2ProjectConfig = {
|
|
2
|
+
name?: string;
|
|
3
|
+
alias?: string;
|
|
4
|
+
id?: string;
|
|
5
|
+
base_url?: string;
|
|
6
|
+
project_key?: string;
|
|
7
|
+
project_name?: string;
|
|
8
|
+
project_token?: string;
|
|
9
|
+
personal_token?: string;
|
|
10
|
+
source?: string;
|
|
11
|
+
};
|
|
12
|
+
export type HubV2Config = HubV2ProjectConfig & {
|
|
13
|
+
default_project?: string;
|
|
14
|
+
projects?: Record<string, HubV2ProjectConfig>;
|
|
15
|
+
};
|
|
16
|
+
export type HubV2ResolveOptions = {
|
|
17
|
+
project?: string;
|
|
18
|
+
projectKey?: string;
|
|
19
|
+
};
|
|
20
|
+
export type HubV2ResolvedContext = {
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
projectKey: string;
|
|
23
|
+
token: string;
|
|
24
|
+
source: string;
|
|
25
|
+
};
|
|
26
|
+
type TokenKind = "project" | "personal";
|
|
27
|
+
export declare function parseJsonObject(content: string): Record<string, unknown>;
|
|
28
|
+
export declare function loadJsonFile(path: string): Record<string, unknown>;
|
|
29
|
+
export declare function normalizeConfig(rawValue: unknown): HubV2Config;
|
|
30
|
+
export declare function loadConfig(pathValue?: string): HubV2Config;
|
|
31
|
+
export declare function resolveHubV2Context(options: HubV2ResolveOptions, tokenKind: TokenKind, pathValue?: string): HubV2ResolvedContext;
|
|
32
|
+
export declare function listConfiguredProjects(project?: string, pathValue?: string): Record<string, unknown>;
|
|
33
|
+
export declare function getConfiguredProject(options: HubV2ResolveOptions, pathValue?: string): Record<string, unknown>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseJsonObject = parseJsonObject;
|
|
4
|
+
exports.loadJsonFile = loadJsonFile;
|
|
5
|
+
exports.normalizeConfig = normalizeConfig;
|
|
6
|
+
exports.loadConfig = loadConfig;
|
|
7
|
+
exports.resolveHubV2Context = resolveHubV2Context;
|
|
8
|
+
exports.listConfiguredProjects = listConfiguredProjects;
|
|
9
|
+
exports.getConfiguredProject = getConfiguredProject;
|
|
10
|
+
const fs_1 = require("fs");
|
|
11
|
+
const os_1 = require("os");
|
|
12
|
+
const path_1 = require("path");
|
|
13
|
+
function asRecord(value) {
|
|
14
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
15
|
+
? value
|
|
16
|
+
: {};
|
|
17
|
+
}
|
|
18
|
+
function getString(record, ...keys) {
|
|
19
|
+
for (const key of keys) {
|
|
20
|
+
const value = record[key];
|
|
21
|
+
if (value !== undefined && value !== null && String(value).trim()) {
|
|
22
|
+
return String(value).trim();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function putIfValue(target, key, value) {
|
|
28
|
+
if (value !== undefined && value !== null && String(value).trim()) {
|
|
29
|
+
target[key] = String(value).trim();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function envValue(...names) {
|
|
33
|
+
for (const name of names) {
|
|
34
|
+
const value = process.env[name];
|
|
35
|
+
if (value !== undefined && value !== null && String(value).trim()) {
|
|
36
|
+
return String(value).trim();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
function stripJsonComments(input) {
|
|
42
|
+
let output = "";
|
|
43
|
+
let inString = false;
|
|
44
|
+
let stringQuote = "";
|
|
45
|
+
let escaped = false;
|
|
46
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
47
|
+
const char = input[index];
|
|
48
|
+
const next = input[index + 1];
|
|
49
|
+
if (inString) {
|
|
50
|
+
output += char;
|
|
51
|
+
if (escaped) {
|
|
52
|
+
escaped = false;
|
|
53
|
+
}
|
|
54
|
+
else if (char === "\\") {
|
|
55
|
+
escaped = true;
|
|
56
|
+
}
|
|
57
|
+
else if (char === stringQuote) {
|
|
58
|
+
inString = false;
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (char === '"' || char === "'") {
|
|
63
|
+
inString = true;
|
|
64
|
+
stringQuote = char;
|
|
65
|
+
output += char;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (char === "/" && next === "/") {
|
|
69
|
+
while (index < input.length && input[index] !== "\n") {
|
|
70
|
+
index += 1;
|
|
71
|
+
}
|
|
72
|
+
output += "\n";
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (char === "/" && next === "*") {
|
|
76
|
+
index += 2;
|
|
77
|
+
while (index < input.length && !(input[index] === "*" && input[index + 1] === "/")) {
|
|
78
|
+
index += 1;
|
|
79
|
+
}
|
|
80
|
+
index += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
output += char;
|
|
84
|
+
}
|
|
85
|
+
return output;
|
|
86
|
+
}
|
|
87
|
+
function parseJsonObject(content) {
|
|
88
|
+
const parsed = JSON.parse(stripJsonComments(content));
|
|
89
|
+
return asRecord(parsed);
|
|
90
|
+
}
|
|
91
|
+
function loadJsonFile(path) {
|
|
92
|
+
if (!(0, fs_1.existsSync)(path)) {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
return parseJsonObject((0, fs_1.readFileSync)(path, "utf8"));
|
|
96
|
+
}
|
|
97
|
+
function mergeHubObject(target, value) {
|
|
98
|
+
const source = asRecord(value);
|
|
99
|
+
putIfValue(target, "base_url", getString(source, "base_url", "baseUrl"));
|
|
100
|
+
putIfValue(target, "project_key", getString(source, "project_key", "projectKey"));
|
|
101
|
+
putIfValue(target, "project_name", getString(source, "project_name", "projectName"));
|
|
102
|
+
putIfValue(target, "project_token", getString(source, "project_token", "projectToken", "token"));
|
|
103
|
+
putIfValue(target, "personal_token", getString(source, "personal_token", "personalToken"));
|
|
104
|
+
putIfValue(target, "source", getString(source, "source"));
|
|
105
|
+
putIfValue(target, "default_project", getString(source, "default_project", "defaultProject", "project"));
|
|
106
|
+
}
|
|
107
|
+
function mergeEnvObject(target, value) {
|
|
108
|
+
const env = asRecord(value);
|
|
109
|
+
putIfValue(target, "base_url", getString(env, "HUB_V2_BASE_URL"));
|
|
110
|
+
putIfValue(target, "project_key", getString(env, "HUB_V2_PROJECT_KEY"));
|
|
111
|
+
putIfValue(target, "project_token", getString(env, "HUB_V2_PROJECT_TOKEN"));
|
|
112
|
+
putIfValue(target, "personal_token", getString(env, "HUB_V2_PERSONAL_TOKEN"));
|
|
113
|
+
putIfValue(target, "source", getString(env, "HUB_V2_SOURCE"));
|
|
114
|
+
putIfValue(target, "default_project", getString(env, "HUB_V2_PROJECT"));
|
|
115
|
+
}
|
|
116
|
+
function normalizeProject(raw) {
|
|
117
|
+
const config = normalizeConfig(raw);
|
|
118
|
+
const record = asRecord(raw);
|
|
119
|
+
putIfValue(config, "name", record.name);
|
|
120
|
+
putIfValue(config, "alias", record.alias);
|
|
121
|
+
putIfValue(config, "id", record.id);
|
|
122
|
+
delete config.projects;
|
|
123
|
+
delete config.default_project;
|
|
124
|
+
return config;
|
|
125
|
+
}
|
|
126
|
+
function normalizeProjects(raw) {
|
|
127
|
+
const value = raw.projects;
|
|
128
|
+
const projects = {};
|
|
129
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
130
|
+
for (const [name, projectValue] of Object.entries(value)) {
|
|
131
|
+
const project = normalizeProject(projectValue);
|
|
132
|
+
const projectName = project.name ?? project.alias ?? name.trim();
|
|
133
|
+
if (projectName) {
|
|
134
|
+
projects[projectName] = project;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
for (const projectValue of value) {
|
|
140
|
+
const project = normalizeProject(projectValue);
|
|
141
|
+
const projectName = project.name ?? project.alias ?? project.id;
|
|
142
|
+
if (projectName) {
|
|
143
|
+
projects[projectName] = project;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return projects;
|
|
148
|
+
}
|
|
149
|
+
function mergeProjects(...maps) {
|
|
150
|
+
const merged = {};
|
|
151
|
+
for (const map of maps) {
|
|
152
|
+
for (const [name, project] of Object.entries(map)) {
|
|
153
|
+
merged[name] = { ...(merged[name] ?? {}), ...project };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return merged;
|
|
157
|
+
}
|
|
158
|
+
function normalizeConfig(rawValue) {
|
|
159
|
+
const raw = asRecord(rawValue);
|
|
160
|
+
const normalized = {};
|
|
161
|
+
mergeHubObject(normalized, raw);
|
|
162
|
+
mergeHubObject(normalized, raw.hubV2);
|
|
163
|
+
mergeEnvObject(normalized, raw.env);
|
|
164
|
+
const projects = mergeProjects(normalizeProjects(raw), normalizeProjects(asRecord(raw.hubV2)));
|
|
165
|
+
if (Object.keys(projects).length) {
|
|
166
|
+
normalized.projects = projects;
|
|
167
|
+
}
|
|
168
|
+
return normalized;
|
|
169
|
+
}
|
|
170
|
+
function configSearchPaths() {
|
|
171
|
+
const envConfig = envValue("HUB_V2_CONFIG");
|
|
172
|
+
if (envConfig) {
|
|
173
|
+
return [(0, path_1.resolve)(envConfig)];
|
|
174
|
+
}
|
|
175
|
+
return [(0, path_1.join)((0, os_1.homedir)(), ".ng-manager", "agent-connections.json")];
|
|
176
|
+
}
|
|
177
|
+
function loadConfig(pathValue) {
|
|
178
|
+
const paths = pathValue ? [(0, path_1.resolve)(pathValue)] : configSearchPaths();
|
|
179
|
+
for (const path of paths) {
|
|
180
|
+
const config = normalizeConfig(loadJsonFile(path));
|
|
181
|
+
if (Object.keys(config).length) {
|
|
182
|
+
return config;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
function selectedProjectConfig(config, options) {
|
|
188
|
+
const projects = config.projects ?? {};
|
|
189
|
+
const entries = Object.entries(projects);
|
|
190
|
+
if (!entries.length) {
|
|
191
|
+
return {};
|
|
192
|
+
}
|
|
193
|
+
const selected = options.project ?? envValue("HUB_V2_PROJECT") ?? config.default_project;
|
|
194
|
+
if (selected) {
|
|
195
|
+
const project = projects[selected];
|
|
196
|
+
if (!project) {
|
|
197
|
+
throw new Error(`project config not found: ${selected}. Available projects: ${entries.map(([name]) => name).sort().join(", ")}`);
|
|
198
|
+
}
|
|
199
|
+
return project;
|
|
200
|
+
}
|
|
201
|
+
if (entries.length === 1) {
|
|
202
|
+
return entries[0][1];
|
|
203
|
+
}
|
|
204
|
+
throw new Error(`multiple projects configured; pass project. Available projects: ${entries.map(([name]) => name).sort().join(", ")}`);
|
|
205
|
+
}
|
|
206
|
+
function configValue(config, projectConfig, key) {
|
|
207
|
+
return projectConfig[key] ?? config[key];
|
|
208
|
+
}
|
|
209
|
+
function resolveHubV2Context(options, tokenKind, pathValue) {
|
|
210
|
+
const config = loadConfig(pathValue);
|
|
211
|
+
const projectConfig = selectedProjectConfig(config, options);
|
|
212
|
+
const baseUrl = (envValue("HUB_V2_BASE_URL") ??
|
|
213
|
+
configValue(config, projectConfig, "base_url"))?.replace(/\/+$/, "");
|
|
214
|
+
const projectKey = options.projectKey ??
|
|
215
|
+
envValue("HUB_V2_PROJECT_KEY") ??
|
|
216
|
+
configValue(config, projectConfig, "project_key");
|
|
217
|
+
const projectToken = envValue("HUB_V2_PROJECT_TOKEN") ??
|
|
218
|
+
configValue(config, projectConfig, "project_token");
|
|
219
|
+
const personalToken = envValue("HUB_V2_PERSONAL_TOKEN") ??
|
|
220
|
+
configValue(config, projectConfig, "personal_token");
|
|
221
|
+
const source = envValue("HUB_V2_SOURCE") ??
|
|
222
|
+
configValue(config, projectConfig, "source") ??
|
|
223
|
+
"ngm-mcp";
|
|
224
|
+
if (!baseUrl) {
|
|
225
|
+
throw new Error("HUB_V2_BASE_URL is required");
|
|
226
|
+
}
|
|
227
|
+
if (!projectKey) {
|
|
228
|
+
throw new Error("HUB_V2_PROJECT_KEY is required");
|
|
229
|
+
}
|
|
230
|
+
const token = tokenKind === "project" ? projectToken : personalToken;
|
|
231
|
+
if (!token) {
|
|
232
|
+
throw new Error(tokenKind === "project" ? "HUB_V2_PROJECT_TOKEN is required" : "HUB_V2_PERSONAL_TOKEN is required");
|
|
233
|
+
}
|
|
234
|
+
return { baseUrl, projectKey, token, source };
|
|
235
|
+
}
|
|
236
|
+
function projectSummary(name, config, project) {
|
|
237
|
+
return {
|
|
238
|
+
name,
|
|
239
|
+
projectName: project.project_name,
|
|
240
|
+
projectKey: project.project_key,
|
|
241
|
+
baseUrl: project.base_url ?? config.base_url,
|
|
242
|
+
hasProjectToken: Boolean(project.project_token ?? config.project_token),
|
|
243
|
+
hasPersonalToken: Boolean(project.personal_token ?? config.personal_token),
|
|
244
|
+
isDefault: name === config.default_project,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function listConfiguredProjects(project, pathValue) {
|
|
248
|
+
const config = loadConfig(pathValue);
|
|
249
|
+
const envProjectKey = envValue("HUB_V2_PROJECT_KEY") ?? config.project_key;
|
|
250
|
+
let items;
|
|
251
|
+
if (envProjectKey) {
|
|
252
|
+
items = [
|
|
253
|
+
{
|
|
254
|
+
name: envValue("HUB_V2_PROJECT") ?? config.default_project ?? "default",
|
|
255
|
+
projectName: config.project_name,
|
|
256
|
+
projectKey: envProjectKey,
|
|
257
|
+
baseUrl: envValue("HUB_V2_BASE_URL") ?? config.base_url,
|
|
258
|
+
hasProjectToken: Boolean(envValue("HUB_V2_PROJECT_TOKEN") ?? config.project_token),
|
|
259
|
+
hasPersonalToken: Boolean(envValue("HUB_V2_PERSONAL_TOKEN") ?? config.personal_token),
|
|
260
|
+
isDefault: true,
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const projects = config.projects ?? {};
|
|
266
|
+
items = Object.entries(projects).map(([name, item]) => projectSummary(name, config, item));
|
|
267
|
+
}
|
|
268
|
+
const filtered = project
|
|
269
|
+
? items.filter((item) => item.name === project || item.projectName === project || item.projectKey === project)
|
|
270
|
+
: items;
|
|
271
|
+
return {
|
|
272
|
+
items: filtered,
|
|
273
|
+
total: filtered.length,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function getConfiguredProject(options, pathValue) {
|
|
277
|
+
const config = loadConfig(pathValue);
|
|
278
|
+
const selected = options.project ?? options.projectKey ?? envValue("HUB_V2_PROJECT") ?? config.default_project;
|
|
279
|
+
if (selected) {
|
|
280
|
+
const projects = listConfiguredProjects(selected, pathValue).items;
|
|
281
|
+
if (projects.length === 1) {
|
|
282
|
+
return projects[0];
|
|
283
|
+
}
|
|
284
|
+
if (!projects.length) {
|
|
285
|
+
throw new Error("Hub V2 project config not found");
|
|
286
|
+
}
|
|
287
|
+
throw new Error("multiple Hub V2 project configs matched");
|
|
288
|
+
}
|
|
289
|
+
const projects = listConfiguredProjects(options.project ?? options.projectKey, pathValue).items;
|
|
290
|
+
if (projects.length === 1) {
|
|
291
|
+
return projects[0];
|
|
292
|
+
}
|
|
293
|
+
if (!projects.length) {
|
|
294
|
+
throw new Error("Hub V2 project config not found");
|
|
295
|
+
}
|
|
296
|
+
throw new Error("multiple Hub V2 project configs matched");
|
|
297
|
+
}
|