@ainecto/mcp 0.1.3 → 0.1.4

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
@@ -43,6 +43,8 @@ Endpoint resolution priority:
43
43
  3. `--env dev`
44
44
  4. production default, `https://ainecto.com/mcp`
45
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.
47
+
46
48
  ## Connector Mode
47
49
 
48
50
  ```bash
@@ -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://ainecto.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,66 @@ 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://ainecto.com/mcp",
88
+ dev: "https://dev.ainecto.com/mcp"
89
+ };
90
+ function resolveEndpoint(input = {}) {
91
+ const envVars = input.envVars ?? process.env;
92
+ const env = input.env ?? "prod";
93
+ if (input.endpoint) {
94
+ return { env, endpoint: normalizeEndpoint(input.endpoint), source: "flag" };
95
+ }
96
+ if (envVars.AINECTO_MCP_ENDPOINT) {
97
+ return { env, endpoint: normalizeEndpoint(envVars.AINECTO_MCP_ENDPOINT), source: "env" };
98
+ }
99
+ return { env, endpoint: DEFAULT_ENDPOINTS[env], source: "default" };
100
+ }
101
+ function normalizeEndpoint(endpoint) {
102
+ const url = new URL(endpoint);
103
+ assertTrustedEndpointUrl(url, "MCP endpoint");
104
+ url.hash = "";
105
+ return url.toString().replace(/\/$/, "");
106
+ }
107
+ function isDefaultEndpoint(endpoint) {
108
+ const normalized = normalizeEndpoint(endpoint);
109
+ return Object.values(DEFAULT_ENDPOINTS).includes(normalized);
110
+ }
111
+ function assertTrustedEndpointUrl(value, label = "URL") {
112
+ const url = typeof value === "string" ? new URL(value) : value;
113
+ if (url.protocol === "https:") {
114
+ return;
115
+ }
116
+ if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
117
+ return;
118
+ }
119
+ throw new Error(`${label} must use https, except http is allowed for localhost loopback endpoints.`);
120
+ }
121
+ function assertLoopbackRedirectUrl(value) {
122
+ const url = new URL(value);
123
+ if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
124
+ return;
125
+ }
126
+ throw new Error("OAuth redirect URI must be an http localhost loopback URL.");
127
+ }
128
+ function isLoopbackHostname(hostname) {
129
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
130
+ }
131
+ function assertEnv(value) {
132
+ if (value === void 0) {
133
+ return void 0;
134
+ }
135
+ if (value === "prod" || value === "dev") {
136
+ return value;
137
+ }
138
+ throw new Error(`Invalid --env value "${value}". Expected "prod" or "dev".`);
139
+ }
140
+
66
141
  // src/core/auth/clientRegistration.ts
67
142
  async function registerPublicClient(registrationEndpoint, redirectUri, fetchImpl = fetch) {
143
+ assertTrustedEndpointUrl(registrationEndpoint, "OAuth client registration endpoint");
144
+ assertLoopbackRedirectUrl(redirectUri);
68
145
  const response = await fetchImpl(registrationEndpoint, {
69
146
  method: "POST",
70
147
  headers: { "content-type": "application/json", accept: "application/json" },
@@ -107,6 +184,7 @@ var OAuthClient = class {
107
184
  async getAccessToken() {
108
185
  const envToken = this.envVars.AINECTO_TOKEN;
109
186
  if (envToken) {
187
+ assertEnvTokenEndpointAllowed(this.options.endpoint, this.envVars);
110
188
  return envToken;
111
189
  }
112
190
  const stored = await this.options.tokenStore.load(this.options.endpoint);
@@ -140,6 +218,7 @@ var OAuthClient = class {
140
218
  authorizeUrl.searchParams.set("code_challenge_method", "S256");
141
219
  authorizeUrl.searchParams.set("resource", this.options.endpoint);
142
220
  authorizeUrl.searchParams.set("state", state);
221
+ assertTrustedEndpointUrl(authorizeUrl, "OAuth authorization URL");
143
222
  await this.openBrowserImpl(authorizeUrl.toString());
144
223
  const code = await loopback.waitForCode();
145
224
  const token = await exchangeToken(metadata.tokenEndpoint, {
@@ -193,18 +272,30 @@ function shouldRefresh(token, now = Date.now()) {
193
272
  return token.expiresAt - now < 6e4;
194
273
  }
195
274
  async function discoverOAuthMetadata(endpoint, fetchImpl = fetch) {
275
+ assertTrustedEndpointUrl(endpoint, "MCP endpoint");
196
276
  const resourceMetadataUrl = await discoverResourceMetadataUrl(endpoint, fetchImpl);
277
+ assertTrustedEndpointUrl(resourceMetadataUrl, "OAuth protected resource metadata URL");
197
278
  const resourceMetadata = await fetchJson(resourceMetadataUrl, fetchImpl);
198
279
  const authorizationServers = resourceMetadata.authorization_servers;
199
280
  if (!Array.isArray(authorizationServers) || typeof authorizationServers[0] !== "string") {
200
281
  throw new Error("OAuth protected resource metadata did not include authorization_servers.");
201
282
  }
202
283
  const authServer = authorizationServers[0];
284
+ assertTrustedEndpointUrl(authServer, "OAuth authorization server");
203
285
  const metadataUrl = authServer.includes("/.well-known/") ? authServer : `${authServer.replace(/\/$/, "")}/.well-known/oauth-authorization-server`;
286
+ assertTrustedEndpointUrl(metadataUrl, "OAuth authorization server metadata URL");
204
287
  const metadata = await fetchJson(metadataUrl, fetchImpl);
205
288
  if (typeof metadata.authorization_endpoint !== "string" || typeof metadata.token_endpoint !== "string") {
206
289
  throw new Error("OAuth authorization server metadata is missing authorization_endpoint or token_endpoint.");
207
290
  }
291
+ assertTrustedEndpointUrl(metadata.authorization_endpoint, "OAuth authorization endpoint");
292
+ assertTrustedEndpointUrl(metadata.token_endpoint, "OAuth token endpoint");
293
+ if (typeof metadata.registration_endpoint === "string") {
294
+ assertTrustedEndpointUrl(metadata.registration_endpoint, "OAuth client registration endpoint");
295
+ }
296
+ if (typeof metadata.issuer === "string") {
297
+ assertTrustedEndpointUrl(metadata.issuer, "OAuth issuer");
298
+ }
208
299
  const codeChallengeMethodsSupported = parseStringArray(metadata.code_challenge_methods_supported);
209
300
  if (!codeChallengeMethodsSupported.includes("S256")) {
210
301
  throw new Error("OAuth authorization server metadata must support PKCE S256.");
@@ -219,20 +310,25 @@ async function discoverOAuthMetadata(endpoint, fetchImpl = fetch) {
219
310
  }
220
311
  async function discoverResourceMetadataUrl(endpoint, fetchImpl) {
221
312
  const challengeUrl = new URL(endpoint);
313
+ assertTrustedEndpointUrl(challengeUrl, "MCP endpoint");
222
314
  const response = await fetchImpl(challengeUrl, { method: "GET" });
223
315
  const challenge = response.headers.get("www-authenticate");
224
316
  const fromChallenge = challenge ? parseBearerChallengeParam(challenge, "resource_metadata") : void 0;
225
317
  if (fromChallenge) {
318
+ assertTrustedEndpointUrl(fromChallenge, "OAuth protected resource metadata URL");
226
319
  return fromChallenge;
227
320
  }
228
321
  const origin = new URL(endpoint).origin;
229
- return `${origin}/.well-known/oauth-protected-resource`;
322
+ const fallback = `${origin}/.well-known/oauth-protected-resource`;
323
+ assertTrustedEndpointUrl(fallback, "OAuth protected resource metadata URL");
324
+ return fallback;
230
325
  }
231
326
  function parseBearerChallengeParam(header, name) {
232
327
  const pattern = new RegExp(`${name}="([^"]+)"`);
233
328
  return pattern.exec(header)?.[1];
234
329
  }
235
330
  async function exchangeToken(tokenEndpoint, params, fetchImpl) {
331
+ assertTrustedEndpointUrl(tokenEndpoint, "OAuth token endpoint");
236
332
  const response = await fetchImpl(tokenEndpoint, {
237
333
  method: "POST",
238
334
  headers: { "content-type": "application/x-www-form-urlencoded" },
@@ -311,48 +407,36 @@ function parseStringArray(value) {
311
407
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
312
408
  }
313
409
  async function fetchJson(url, fetchImpl) {
410
+ assertTrustedEndpointUrl(url, "OAuth metadata URL");
314
411
  const response = await fetchImpl(url);
315
412
  if (!response.ok) {
316
413
  throw new Error(`Failed to fetch OAuth metadata from ${url}: HTTP ${response.status}.`);
317
414
  }
318
415
  return response.json();
319
416
  }
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" };
417
+ function getBrowserOpenCommand(url, platform = process.platform) {
418
+ assertTrustedEndpointUrl(url, "OAuth authorization URL");
419
+ if (platform === "darwin") {
420
+ return { command: "open", args: [url] };
337
421
  }
338
- if (envVars.AINECTO_MCP_ENDPOINT) {
339
- return { env, endpoint: normalizeEndpoint(envVars.AINECTO_MCP_ENDPOINT), source: "env" };
422
+ if (platform === "win32") {
423
+ return { command: "explorer.exe", args: [url] };
340
424
  }
341
- return { env, endpoint: DEFAULT_ENDPOINTS[env], source: "default" };
425
+ return { command: "xdg-open", args: [url] };
342
426
  }
343
- function normalizeEndpoint(endpoint) {
344
- const url = new URL(endpoint);
345
- url.hash = "";
346
- return url.toString().replace(/\/$/, "");
427
+ async function openBrowser(url) {
428
+ const { command, args } = getBrowserOpenCommand(url);
429
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
430
+ child.unref();
347
431
  }
348
- function assertEnv(value) {
349
- if (value === void 0) {
350
- return void 0;
432
+ function assertEnvTokenEndpointAllowed(endpoint, envVars) {
433
+ if (envVars.AINECTO_ALLOW_CUSTOM_ENDPOINT_TOKEN === "1") {
434
+ return;
351
435
  }
352
- if (value === "prod" || value === "dev") {
353
- return value;
436
+ if (isDefaultEndpoint(endpoint)) {
437
+ return;
354
438
  }
355
- throw new Error(`Invalid --env value "${value}". Expected "prod" or "dev".`);
439
+ 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
440
  }
357
441
 
358
442
  // src/core/input/payloadSource.ts
@@ -478,10 +562,11 @@ var McpRpcClient = class {
478
562
  }
479
563
  const text = await response.text();
480
564
  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
- });
565
+ throw new McpRpcError(
566
+ `MCP HTTP request failed with HTTP ${response.status}.`,
567
+ "MCP_HTTP_ERROR",
568
+ httpErrorDetails(response.status, text, this.options.envVars ?? process.env)
569
+ );
485
570
  }
486
571
  if (!text.trim()) {
487
572
  return void 0;
@@ -513,6 +598,13 @@ function isJsonRpcResponse(value) {
513
598
  function isRecord2(value) {
514
599
  return typeof value === "object" && value !== null && !Array.isArray(value);
515
600
  }
601
+ function httpErrorDetails(status, body, envVars) {
602
+ const details = { status };
603
+ if (envVars.AINECTO_CLI_DEBUG === "1" || envVars.AINECTO_DEBUG === "1") {
604
+ details.body = safeBodyPreview(body);
605
+ }
606
+ return details;
607
+ }
516
608
  function safeBodyPreview(body) {
517
609
  return body.length > 500 ? `${body.slice(0, 500)}...` : body;
518
610
  }
@@ -549,12 +641,15 @@ function toErrorShape(error) {
549
641
  error: {
550
642
  code: maybe.code ?? "CLI_ERROR",
551
643
  message: error.message,
552
- details: maybe.details
644
+ details: redactSecrets(maybe.details)
553
645
  }
554
646
  };
555
647
  }
556
648
  return { ok: false, error: { code: "CLI_ERROR", message: String(error) } };
557
649
  }
650
+ function redactSecrets(value) {
651
+ return redactSecretsInner(value, /* @__PURE__ */ new WeakSet());
652
+ }
558
653
  function renderHuman(data) {
559
654
  if (Array.isArray(data)) {
560
655
  return data.map((item) => renderHuman(item)).join("\n");
@@ -564,6 +659,33 @@ function renderHuman(data) {
564
659
  }
565
660
  return JSON.stringify(data, null, 2);
566
661
  }
662
+ function redactSecretsInner(value, seen) {
663
+ if (typeof value === "string") {
664
+ return redactSecretString(value);
665
+ }
666
+ if (Array.isArray(value)) {
667
+ return value.map((item) => redactSecretsInner(item, seen));
668
+ }
669
+ if (value && typeof value === "object") {
670
+ if (seen.has(value)) {
671
+ return "[circular]";
672
+ }
673
+ seen.add(value);
674
+ const output = {};
675
+ for (const [key, child] of Object.entries(value)) {
676
+ output[key] = isSecretKey(key) ? "[redacted]" : redactSecretsInner(child, seen);
677
+ }
678
+ return output;
679
+ }
680
+ return value;
681
+ }
682
+ function isSecretKey(key) {
683
+ const normalized = key.toLowerCase().replace(/-/g, "_");
684
+ return normalized === "authorization" || normalized.includes("access_token") || normalized.includes("refresh_token") || normalized.includes("client_secret") || normalized.includes("token");
685
+ }
686
+ function redactSecretString(value) {
687
+ 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]");
688
+ }
567
689
 
568
690
  // src/core/catalog/generated.prod.ts
569
691
  var generatedTools = [