@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 +6 -4
- package/dist/bin/ainecto.js +158 -36
- package/dist/bin/ainecto.js.map +1 -1
- package/dist/bin/mcp.js +130 -8
- package/dist/bin/mcp.js.map +1 -1
- package/package.json +1 -1
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
|
|
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.
|
|
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.
|
|
130
|
-
npm exec --package ./ainecto-mcp-0.1.
|
|
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
|
package/dist/bin/ainecto.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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 (
|
|
339
|
-
return {
|
|
422
|
+
if (platform === "win32") {
|
|
423
|
+
return { command: "explorer.exe", args: [url] };
|
|
340
424
|
}
|
|
341
|
-
return {
|
|
425
|
+
return { command: "xdg-open", args: [url] };
|
|
342
426
|
}
|
|
343
|
-
function
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
349
|
-
if (
|
|
350
|
-
return
|
|
432
|
+
function assertEnvTokenEndpointAllowed(endpoint, envVars) {
|
|
433
|
+
if (envVars.AINECTO_ALLOW_CUSTOM_ENDPOINT_TOKEN === "1") {
|
|
434
|
+
return;
|
|
351
435
|
}
|
|
352
|
-
if (
|
|
353
|
-
return
|
|
436
|
+
if (isDefaultEndpoint(endpoint)) {
|
|
437
|
+
return;
|
|
354
438
|
}
|
|
355
|
-
throw new Error(
|
|
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(
|
|
482
|
-
|
|
483
|
-
|
|
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 = [
|