@ainecto/mcp 0.1.3 → 0.2.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.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # @ainecto/mcp
1
+ # @ai-erd/mcp
2
2
 
3
- Ainecto MCP connector and first-party CLI.
3
+ AI-ERD MCP connector and first-party CLI.
4
4
 
5
5
  This package exposes two entry points:
6
6
 
7
- - `npx -y @ainecto/mcp` or `ainecto mcp`: raw stdio-to-HTTP JSON-RPC proxy for Ainecto MCP.
7
+ - `npx -y @ai-erd/mcp` or `ainecto mcp`: raw stdio-to-HTTP JSON-RPC proxy for AI-ERD MCP.
8
8
  - `ainecto <command>`: human CLI for auth and low-level tool calls.
9
9
 
10
10
  Phase 1 keeps the connector as a raw proxy. File, stdin, and inline JSON parsing are only available in first-party CLI commands.
@@ -41,12 +41,14 @@ Endpoint resolution priority:
41
41
  1. `--endpoint <url>`
42
42
  2. `AINECTO_MCP_ENDPOINT`
43
43
  3. `--env dev`
44
- 4. production default, `https://ainecto.com/mcp`
44
+ 4. production default, `https://ai-erd.com/mcp`
45
+
46
+ Endpoint URLs must use `https:`. Plain `http:` is accepted only for localhost loopback targets such as `127.0.0.1`. When `AINECTO_TOKEN` is set, the CLI only sends it to the default prod/dev endpoints unless `AINECTO_ALLOW_CUSTOM_ENDPOINT_TOKEN=1` is set for an explicitly trusted custom endpoint.
45
47
 
46
48
  ## Connector Mode
47
49
 
48
50
  ```bash
49
- npx -y @ainecto/mcp
51
+ npx -y @ai-erd/mcp
50
52
  ainecto mcp --env dev
51
53
  ```
52
54
 
@@ -61,7 +63,7 @@ Use the direct package configuration until the server is published to the offici
61
63
  "mcpServers": {
62
64
  "ainecto": {
63
65
  "command": "npx",
64
- "args": ["-y", "@ainecto/mcp"]
66
+ "args": ["-y", "@ai-erd/mcp"]
65
67
  }
66
68
  }
67
69
  }
@@ -74,13 +76,13 @@ For a non-production endpoint, pass CLI flags through the package args:
74
76
  "mcpServers": {
75
77
  "ainecto-dev": {
76
78
  "command": "npx",
77
- "args": ["-y", "@ainecto/mcp", "--env", "dev"]
79
+ "args": ["-y", "@ai-erd/mcp", "--env", "dev"]
78
80
  }
79
81
  }
80
82
  }
81
83
  ```
82
84
 
83
- The planned official Registry server name is `io.github.codelives/ainecto`, backed by the public npm package `@ainecto/mcp`. Registry publication requires the npm package version referenced by `server.json` to include a matching `mcpName` field in `package.json`.
85
+ The planned official Registry server name is `io.github.codelives/ainecto`, backed by the public npm package `@ai-erd/mcp`. Registry publication requires the npm package version referenced by `server.json` to include a matching `mcpName` field in `package.json`.
84
86
 
85
87
  ## Catalog Sync
86
88
 
@@ -92,9 +94,9 @@ AINECTO_CATALOG_SYNC_TOKEN=... npm run sync:tools -- --env prod --check
92
94
  AINECTO_CATALOG_SYNC_TOKEN=... npm run sync:tools -- --env dev --check
93
95
  ```
94
96
 
95
- The checked-in generated catalogs are deterministic output from `tools/list`. The dev catalog has been live-synced from `https://dev.ainecto.com/mcp`; the prod catalog remains a seed fixture until prod auth is available. Presentation metadata lives separately in `src/core/catalog/enrichments.ts`.
97
+ The checked-in generated catalogs are deterministic output from `tools/list`. The prod and dev catalogs are live-synced snapshots from `https://ai-erd.com/mcp` and `https://dev.ainecto.com/mcp`. Presentation metadata lives separately in `src/core/catalog/enrichments.ts`.
96
98
 
97
- `ainecto tools catalog` prints the local generated catalog. Prod output is fixture-only until authenticated prod live sync updates `generated.prod.ts` and `tools-list.prod.json`.
99
+ `ainecto tools catalog` prints the local generated catalog. Rerun authenticated `sync:tools` when prod or dev `tools/list` changes.
98
100
 
99
101
  ## Generated Friendly Commands
100
102
 
@@ -126,8 +128,8 @@ The generated `request_upload_token` and `upload_attachments` paths remain raw M
126
128
 
127
129
  ```bash
128
130
  npm pack
129
- npx -y ./ainecto-mcp-0.1.3.tgz --help
130
- npm exec --package ./ainecto-mcp-0.1.3.tgz -- ainecto --help
131
+ npx -y ./ainecto-mcp-0.1.4.tgz --help
132
+ npm exec --package ./ainecto-mcp-0.1.4.tgz -- ainecto --help
131
133
  ```
132
134
 
133
135
  ## Live Dev MCP Smoke
@@ -26,6 +26,7 @@ var FileTokenStore = class {
26
26
  }
27
27
  async readFile() {
28
28
  try {
29
+ await this.hardenExistingPermissions();
29
30
  const raw = await readFile(this.filePath, "utf8");
30
31
  const parsed = JSON.parse(raw);
31
32
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
@@ -49,6 +50,11 @@ var FileTokenStore = class {
49
50
  await rename(tmpPath, this.filePath);
50
51
  await chmod(this.filePath, 384);
51
52
  }
53
+ async hardenExistingPermissions() {
54
+ const dirPath = dirname(this.filePath);
55
+ await chmodIfPermissive(dirPath, 448);
56
+ await chmodIfPermissive(this.filePath, 384);
57
+ }
52
58
  };
53
59
  function tokenKey(endpoint) {
54
60
  return createHash("sha256").update(endpoint).digest("hex");
@@ -56,6 +62,19 @@ function tokenKey(endpoint) {
56
62
  function isNodeError(error, code) {
57
63
  return error instanceof Error && "code" in error && error.code === code;
58
64
  }
65
+ async function chmodIfPermissive(path, mode) {
66
+ try {
67
+ const result = await stat(path);
68
+ if ((result.mode & 63) !== 0) {
69
+ await chmod(path, mode);
70
+ }
71
+ } catch (error) {
72
+ if (isNodeError(error, "ENOENT")) {
73
+ return;
74
+ }
75
+ throw error;
76
+ }
77
+ }
59
78
 
60
79
  // src/core/auth/oauth.ts
61
80
  import { createHash as createHash2, randomBytes } from "crypto";
@@ -63,8 +82,70 @@ import { createServer } from "http";
63
82
  import { spawn } from "child_process";
64
83
  import { URLSearchParams } from "url";
65
84
 
85
+ // src/core/config/endpoints.ts
86
+ var DEFAULT_ENDPOINTS = {
87
+ prod: "https://ai-erd.com/mcp",
88
+ dev: "https://dev.ai-erd.com/mcp"
89
+ };
90
+ var LEGACY_DEFAULT_ENDPOINTS = [
91
+ "https://ainecto.com/mcp",
92
+ "https://dev.ainecto.com/mcp"
93
+ ];
94
+ function resolveEndpoint(input = {}) {
95
+ const envVars = input.envVars ?? process.env;
96
+ const env = input.env ?? "prod";
97
+ if (input.endpoint) {
98
+ return { env, endpoint: normalizeEndpoint(input.endpoint), source: "flag" };
99
+ }
100
+ if (envVars.AINECTO_MCP_ENDPOINT) {
101
+ return { env, endpoint: normalizeEndpoint(envVars.AINECTO_MCP_ENDPOINT), source: "env" };
102
+ }
103
+ return { env, endpoint: DEFAULT_ENDPOINTS[env], source: "default" };
104
+ }
105
+ function normalizeEndpoint(endpoint) {
106
+ const url = new URL(endpoint);
107
+ assertTrustedEndpointUrl(url, "MCP endpoint");
108
+ url.hash = "";
109
+ return url.toString().replace(/\/$/, "");
110
+ }
111
+ function isDefaultEndpoint(endpoint) {
112
+ const normalized = normalizeEndpoint(endpoint);
113
+ return Object.values(DEFAULT_ENDPOINTS).includes(normalized) || LEGACY_DEFAULT_ENDPOINTS.includes(normalized);
114
+ }
115
+ function assertTrustedEndpointUrl(value, label = "URL") {
116
+ const url = typeof value === "string" ? new URL(value) : value;
117
+ if (url.protocol === "https:") {
118
+ return;
119
+ }
120
+ if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
121
+ return;
122
+ }
123
+ throw new Error(`${label} must use https, except http is allowed for localhost loopback endpoints.`);
124
+ }
125
+ function assertLoopbackRedirectUrl(value) {
126
+ const url = new URL(value);
127
+ if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
128
+ return;
129
+ }
130
+ throw new Error("OAuth redirect URI must be an http localhost loopback URL.");
131
+ }
132
+ function isLoopbackHostname(hostname) {
133
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
134
+ }
135
+ function assertEnv(value) {
136
+ if (value === void 0) {
137
+ return void 0;
138
+ }
139
+ if (value === "prod" || value === "dev") {
140
+ return value;
141
+ }
142
+ throw new Error(`Invalid --env value "${value}". Expected "prod" or "dev".`);
143
+ }
144
+
66
145
  // src/core/auth/clientRegistration.ts
67
146
  async function registerPublicClient(registrationEndpoint, redirectUri, fetchImpl = fetch) {
147
+ assertTrustedEndpointUrl(registrationEndpoint, "OAuth client registration endpoint");
148
+ assertLoopbackRedirectUrl(redirectUri);
68
149
  const response = await fetchImpl(registrationEndpoint, {
69
150
  method: "POST",
70
151
  headers: { "content-type": "application/json", accept: "application/json" },
@@ -107,6 +188,7 @@ var OAuthClient = class {
107
188
  async getAccessToken() {
108
189
  const envToken = this.envVars.AINECTO_TOKEN;
109
190
  if (envToken) {
191
+ assertEnvTokenEndpointAllowed(this.options.endpoint, this.envVars);
110
192
  return envToken;
111
193
  }
112
194
  const stored = await this.options.tokenStore.load(this.options.endpoint);
@@ -140,6 +222,7 @@ var OAuthClient = class {
140
222
  authorizeUrl.searchParams.set("code_challenge_method", "S256");
141
223
  authorizeUrl.searchParams.set("resource", this.options.endpoint);
142
224
  authorizeUrl.searchParams.set("state", state);
225
+ assertTrustedEndpointUrl(authorizeUrl, "OAuth authorization URL");
143
226
  await this.openBrowserImpl(authorizeUrl.toString());
144
227
  const code = await loopback.waitForCode();
145
228
  const token = await exchangeToken(metadata.tokenEndpoint, {
@@ -193,18 +276,30 @@ function shouldRefresh(token, now = Date.now()) {
193
276
  return token.expiresAt - now < 6e4;
194
277
  }
195
278
  async function discoverOAuthMetadata(endpoint, fetchImpl = fetch) {
279
+ assertTrustedEndpointUrl(endpoint, "MCP endpoint");
196
280
  const resourceMetadataUrl = await discoverResourceMetadataUrl(endpoint, fetchImpl);
281
+ assertTrustedEndpointUrl(resourceMetadataUrl, "OAuth protected resource metadata URL");
197
282
  const resourceMetadata = await fetchJson(resourceMetadataUrl, fetchImpl);
198
283
  const authorizationServers = resourceMetadata.authorization_servers;
199
284
  if (!Array.isArray(authorizationServers) || typeof authorizationServers[0] !== "string") {
200
285
  throw new Error("OAuth protected resource metadata did not include authorization_servers.");
201
286
  }
202
287
  const authServer = authorizationServers[0];
288
+ assertTrustedEndpointUrl(authServer, "OAuth authorization server");
203
289
  const metadataUrl = authServer.includes("/.well-known/") ? authServer : `${authServer.replace(/\/$/, "")}/.well-known/oauth-authorization-server`;
290
+ assertTrustedEndpointUrl(metadataUrl, "OAuth authorization server metadata URL");
204
291
  const metadata = await fetchJson(metadataUrl, fetchImpl);
205
292
  if (typeof metadata.authorization_endpoint !== "string" || typeof metadata.token_endpoint !== "string") {
206
293
  throw new Error("OAuth authorization server metadata is missing authorization_endpoint or token_endpoint.");
207
294
  }
295
+ assertTrustedEndpointUrl(metadata.authorization_endpoint, "OAuth authorization endpoint");
296
+ assertTrustedEndpointUrl(metadata.token_endpoint, "OAuth token endpoint");
297
+ if (typeof metadata.registration_endpoint === "string") {
298
+ assertTrustedEndpointUrl(metadata.registration_endpoint, "OAuth client registration endpoint");
299
+ }
300
+ if (typeof metadata.issuer === "string") {
301
+ assertTrustedEndpointUrl(metadata.issuer, "OAuth issuer");
302
+ }
208
303
  const codeChallengeMethodsSupported = parseStringArray(metadata.code_challenge_methods_supported);
209
304
  if (!codeChallengeMethodsSupported.includes("S256")) {
210
305
  throw new Error("OAuth authorization server metadata must support PKCE S256.");
@@ -219,20 +314,25 @@ async function discoverOAuthMetadata(endpoint, fetchImpl = fetch) {
219
314
  }
220
315
  async function discoverResourceMetadataUrl(endpoint, fetchImpl) {
221
316
  const challengeUrl = new URL(endpoint);
317
+ assertTrustedEndpointUrl(challengeUrl, "MCP endpoint");
222
318
  const response = await fetchImpl(challengeUrl, { method: "GET" });
223
319
  const challenge = response.headers.get("www-authenticate");
224
320
  const fromChallenge = challenge ? parseBearerChallengeParam(challenge, "resource_metadata") : void 0;
225
321
  if (fromChallenge) {
322
+ assertTrustedEndpointUrl(fromChallenge, "OAuth protected resource metadata URL");
226
323
  return fromChallenge;
227
324
  }
228
325
  const origin = new URL(endpoint).origin;
229
- return `${origin}/.well-known/oauth-protected-resource`;
326
+ const fallback = `${origin}/.well-known/oauth-protected-resource`;
327
+ assertTrustedEndpointUrl(fallback, "OAuth protected resource metadata URL");
328
+ return fallback;
230
329
  }
231
330
  function parseBearerChallengeParam(header, name) {
232
331
  const pattern = new RegExp(`${name}="([^"]+)"`);
233
332
  return pattern.exec(header)?.[1];
234
333
  }
235
334
  async function exchangeToken(tokenEndpoint, params, fetchImpl) {
335
+ assertTrustedEndpointUrl(tokenEndpoint, "OAuth token endpoint");
236
336
  const response = await fetchImpl(tokenEndpoint, {
237
337
  method: "POST",
238
338
  headers: { "content-type": "application/x-www-form-urlencoded" },
@@ -311,48 +411,36 @@ function parseStringArray(value) {
311
411
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
312
412
  }
313
413
  async function fetchJson(url, fetchImpl) {
414
+ assertTrustedEndpointUrl(url, "OAuth metadata URL");
314
415
  const response = await fetchImpl(url);
315
416
  if (!response.ok) {
316
417
  throw new Error(`Failed to fetch OAuth metadata from ${url}: HTTP ${response.status}.`);
317
418
  }
318
419
  return response.json();
319
420
  }
320
- async function openBrowser(url) {
321
- const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
322
- const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
323
- const child = spawn(command, args, { stdio: "ignore", detached: true });
324
- child.unref();
325
- }
326
-
327
- // src/core/config/endpoints.ts
328
- var DEFAULT_ENDPOINTS = {
329
- prod: "https://ainecto.com/mcp",
330
- dev: "https://dev.ainecto.com/mcp"
331
- };
332
- function resolveEndpoint(input = {}) {
333
- const envVars = input.envVars ?? process.env;
334
- const env = input.env ?? "prod";
335
- if (input.endpoint) {
336
- return { env, endpoint: normalizeEndpoint(input.endpoint), source: "flag" };
421
+ function getBrowserOpenCommand(url, platform = process.platform) {
422
+ assertTrustedEndpointUrl(url, "OAuth authorization URL");
423
+ if (platform === "darwin") {
424
+ return { command: "open", args: [url] };
337
425
  }
338
- if (envVars.AINECTO_MCP_ENDPOINT) {
339
- return { env, endpoint: normalizeEndpoint(envVars.AINECTO_MCP_ENDPOINT), source: "env" };
426
+ if (platform === "win32") {
427
+ return { command: "explorer.exe", args: [url] };
340
428
  }
341
- return { env, endpoint: DEFAULT_ENDPOINTS[env], source: "default" };
429
+ return { command: "xdg-open", args: [url] };
342
430
  }
343
- function normalizeEndpoint(endpoint) {
344
- const url = new URL(endpoint);
345
- url.hash = "";
346
- return url.toString().replace(/\/$/, "");
431
+ async function openBrowser(url) {
432
+ const { command, args } = getBrowserOpenCommand(url);
433
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
434
+ child.unref();
347
435
  }
348
- function assertEnv(value) {
349
- if (value === void 0) {
350
- return void 0;
436
+ function assertEnvTokenEndpointAllowed(endpoint, envVars) {
437
+ if (envVars.AINECTO_ALLOW_CUSTOM_ENDPOINT_TOKEN === "1") {
438
+ return;
351
439
  }
352
- if (value === "prod" || value === "dev") {
353
- return value;
440
+ if (isDefaultEndpoint(endpoint)) {
441
+ return;
354
442
  }
355
- throw new Error(`Invalid --env value "${value}". Expected "prod" or "dev".`);
443
+ throw new Error("AINECTO_TOKEN can only be used with the default prod/dev endpoints. Set AINECTO_ALLOW_CUSTOM_ENDPOINT_TOKEN=1 only when you intentionally trust the custom endpoint.");
356
444
  }
357
445
 
358
446
  // src/core/input/payloadSource.ts
@@ -478,10 +566,11 @@ var McpRpcClient = class {
478
566
  }
479
567
  const text = await response.text();
480
568
  if (!response.ok) {
481
- throw new McpRpcError(`MCP HTTP request failed with HTTP ${response.status}.`, "MCP_HTTP_ERROR", {
482
- status: response.status,
483
- body: safeBodyPreview(text)
484
- });
569
+ throw new McpRpcError(
570
+ `MCP HTTP request failed with HTTP ${response.status}.`,
571
+ "MCP_HTTP_ERROR",
572
+ httpErrorDetails(response.status, text, this.options.envVars ?? process.env)
573
+ );
485
574
  }
486
575
  if (!text.trim()) {
487
576
  return void 0;
@@ -513,6 +602,13 @@ function isJsonRpcResponse(value) {
513
602
  function isRecord2(value) {
514
603
  return typeof value === "object" && value !== null && !Array.isArray(value);
515
604
  }
605
+ function httpErrorDetails(status, body, envVars) {
606
+ const details = { status };
607
+ if (envVars.AINECTO_CLI_DEBUG === "1" || envVars.AINECTO_DEBUG === "1") {
608
+ details.body = safeBodyPreview(body);
609
+ }
610
+ return details;
611
+ }
516
612
  function safeBodyPreview(body) {
517
613
  return body.length > 500 ? `${body.slice(0, 500)}...` : body;
518
614
  }
@@ -549,12 +645,15 @@ function toErrorShape(error) {
549
645
  error: {
550
646
  code: maybe.code ?? "CLI_ERROR",
551
647
  message: error.message,
552
- details: maybe.details
648
+ details: redactSecrets(maybe.details)
553
649
  }
554
650
  };
555
651
  }
556
652
  return { ok: false, error: { code: "CLI_ERROR", message: String(error) } };
557
653
  }
654
+ function redactSecrets(value) {
655
+ return redactSecretsInner(value, /* @__PURE__ */ new WeakSet());
656
+ }
558
657
  function renderHuman(data) {
559
658
  if (Array.isArray(data)) {
560
659
  return data.map((item) => renderHuman(item)).join("\n");
@@ -564,6 +663,33 @@ function renderHuman(data) {
564
663
  }
565
664
  return JSON.stringify(data, null, 2);
566
665
  }
666
+ function redactSecretsInner(value, seen) {
667
+ if (typeof value === "string") {
668
+ return redactSecretString(value);
669
+ }
670
+ if (Array.isArray(value)) {
671
+ return value.map((item) => redactSecretsInner(item, seen));
672
+ }
673
+ if (value && typeof value === "object") {
674
+ if (seen.has(value)) {
675
+ return "[circular]";
676
+ }
677
+ seen.add(value);
678
+ const output = {};
679
+ for (const [key, child] of Object.entries(value)) {
680
+ output[key] = isSecretKey(key) ? "[redacted]" : redactSecretsInner(child, seen);
681
+ }
682
+ return output;
683
+ }
684
+ return value;
685
+ }
686
+ function isSecretKey(key) {
687
+ const normalized = key.toLowerCase().replace(/-/g, "_");
688
+ return normalized === "authorization" || normalized.includes("access_token") || normalized.includes("refresh_token") || normalized.includes("client_secret") || normalized.includes("token");
689
+ }
690
+ function redactSecretString(value) {
691
+ return value.replace(/("(?:authorization|access_token|refresh_token|client_secret|token)"\s*:\s*")([^"]*)(")/gi, "$1[redacted]$3").replace(/((?:authorization|access_token|refresh_token|client_secret|token)=)([^&\s"']+)/gi, "$1[redacted]").replace(/(authorization:\s*bearer\s+)([^\s,}]+)/gi, "$1[redacted]").replace(/(bearer\s+)(eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/gi, "$1[redacted]");
692
+ }
567
693
 
568
694
  // src/core/catalog/generated.prod.ts
569
695
  var generatedTools = [