@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 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
- MCP tools must not call the local Fastify HTTP API. HTTP routes, Electron IPC, CLI commands, and MCP tools should all adapt the same core services.
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
- This MVP only registers read tools. It does not implement arbitrary shell execution, task start/stop/restart, git pull/checkout/commit/reset, proxy reload, runtime install/remove, file deletion, or system environment mutation.
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
- NGM_MCP_ALLOW_WRITE Future policy flag for write tools. Defaults to false.
45
- NGM_MCP_ALLOW_EXECUTE Future policy flag for execute tools. Defaults to false.
46
- NGM_MCP_ALLOW_DANGEROUS Future policy flag for dangerous tools. Defaults to false.
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
- The server uses stdio transport only. It does not listen on an HTTP port, and stdout is reserved for the MCP protocol.
68
-
69
- ## MCP Client Configuration
70
-
71
- Built output:
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
- ## Tools
116
-
117
- Project:
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
- ## Result Shape
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
 
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpToolDefinition } from "../index";
2
+ export declare function hubV2DocsTools(): McpToolDefinition[];