@adrata/adrata-mcp 1.0.3 → 1.0.7
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 +29 -1
- package/access/auth.js +49 -1
- package/access/oauth.js +336 -23
- package/access/tiers.js +15 -0
- package/analytics.js +141 -20
- package/edge-block.js +118 -0
- package/package.json +3 -2
- package/server.js +130 -20
- package/server.json +14 -2
- package/skills/qa-the-card/SKILL.md +218 -0
- package/skills/ship-the-card/SKILL.md +18 -14
- package/tool-annotations.js +10 -2
- package/tools/source-control/connection-tools.js +31 -0
- package/tools/work-board-tools.js +1159 -48
- package/tools/work-hub/audit.js +17 -7
- package/toolsets/revenue/competitive-coverage.js +164 -0
- package/transport-http.js +30 -9
package/analytics.js
CHANGED
|
@@ -119,7 +119,6 @@ function ensureDir() {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function readLocalAnalytics() {
|
|
122
|
-
ensureDir();
|
|
123
122
|
if (!existsSync(ANALYTICS_FILE)) {
|
|
124
123
|
return { events: [], sessions: [] };
|
|
125
124
|
}
|
|
@@ -278,6 +277,69 @@ export function trackConversion(apiFn, auth, fromTier, toTier, trigger) {
|
|
|
278
277
|
/** @type {{ startTime: number, toolsUsed: Set<string>, invocationCount: number } | null} */
|
|
279
278
|
let currentSession = null;
|
|
280
279
|
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Analytics wrapper coverage
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// The analytics counter is a monkey-patch on `server.tool`, so it can only see
|
|
284
|
+
// tools registered AFTER it is installed. That makes its own coverage a fact
|
|
285
|
+
// worth reporting: a zero from an instrumented surface and a zero from an
|
|
286
|
+
// uninstrumented one are different answers, and until 2026-08-29 they were
|
|
287
|
+
// rendered identically. `countedToolNames` is what the wrapper actually
|
|
288
|
+
// wrapped; `get_mcp_analytics` compares it against what the server registered.
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Wrapper state belongs to one McpServer instance. Hosted HTTP creates a fresh
|
|
292
|
+
* server for every protocol session, so a module-global installed flag would
|
|
293
|
+
* wrap the first client and silently skip every client after it.
|
|
294
|
+
*
|
|
295
|
+
* @type {WeakMap<object, { countedToolNames: Set<string> }>}
|
|
296
|
+
*/
|
|
297
|
+
let analyticsWrapperStates = new WeakMap();
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* How much of the registered tool surface the invocation counter can see.
|
|
301
|
+
*
|
|
302
|
+
* @param {object} [server] - McpServer instance, for the registered-tool total
|
|
303
|
+
* @returns {{installed: boolean, tools_counted: number, tools_registered: number|null, complete: boolean|null, uncounted_note: string|undefined}}
|
|
304
|
+
*/
|
|
305
|
+
export function getAnalyticsCoverage(server) {
|
|
306
|
+
const registered = registeredToolCount(server);
|
|
307
|
+
const wrapperState = server && typeof server === 'object'
|
|
308
|
+
? analyticsWrapperStates.get(server)
|
|
309
|
+
: undefined;
|
|
310
|
+
const counted = wrapperState?.countedToolNames.size ?? 0;
|
|
311
|
+
const complete = registered === null ? null : counted >= registered;
|
|
312
|
+
return {
|
|
313
|
+
installed: wrapperState !== undefined,
|
|
314
|
+
tools_counted: counted,
|
|
315
|
+
tools_registered: registered,
|
|
316
|
+
complete,
|
|
317
|
+
uncounted_note:
|
|
318
|
+
complete === false
|
|
319
|
+
? `${registered - counted} of ${registered} registered tools were registered before the `
|
|
320
|
+
+ 'analytics wrapper was installed, so their invocations are NOT counted. '
|
|
321
|
+
+ 'Treat tool_invocations as a lower bound, not a measurement.'
|
|
322
|
+
: undefined,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Best-effort read of the SDK's registered-tool map. Returns null if unavailable. */
|
|
327
|
+
function registeredToolCount(server) {
|
|
328
|
+
try {
|
|
329
|
+
const registry = server?._registeredTools;
|
|
330
|
+
if (registry && typeof registry === 'object') return Object.keys(registry).length;
|
|
331
|
+
} catch {
|
|
332
|
+
// The SDK internal is not a contract. An unknown total is reported as
|
|
333
|
+
// null, never as a number we did not measure.
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Test-only: forget that the wrapper was installed. */
|
|
339
|
+
export function __resetAnalyticsWrapperForTests() {
|
|
340
|
+
analyticsWrapperStates = new WeakMap();
|
|
341
|
+
}
|
|
342
|
+
|
|
281
343
|
/**
|
|
282
344
|
* Start a new analytics session.
|
|
283
345
|
*
|
|
@@ -388,24 +450,43 @@ const SEARCH_TOOLS = {
|
|
|
388
450
|
* invocation. This builds on top of the existing event logging in memory.js
|
|
389
451
|
* by adding richer analytics (search classification, tier gates, sessions).
|
|
390
452
|
*
|
|
391
|
-
* Must be applied AFTER the event logging wrapper from memory.js
|
|
453
|
+
* Must be applied AFTER the event logging wrapper from memory.js, and BEFORE
|
|
454
|
+
* the first `server.tool(...)` call. Monkey-patching `server.tool` can only
|
|
455
|
+
* ever reach FUTURE registrations, so installing it late does not degrade the
|
|
456
|
+
* count — it silently zeroes it. Measured 2026-08-29: installed from
|
|
457
|
+
* `registerAnalytics` ~1975 lines after the first registration, it covered 27
|
|
458
|
+
* of 254 tools and `get_mcp_analytics` reported `tool_invocations.total: 0`
|
|
459
|
+
* with no indication that 89% of the surface was never instrumented.
|
|
460
|
+
*
|
|
461
|
+
* Idempotent: calling it twice installs one wrapper, so `registerAnalytics`
|
|
462
|
+
* remains safe to call after server.js has already installed it early.
|
|
463
|
+
*
|
|
464
|
+
* `auth` may be an AUTH object or a zero-argument getter. Prefer the getter:
|
|
465
|
+
* server.js reassigns AUTH on `connect_workspace`, and an object captured at
|
|
466
|
+
* install time would report the pre-connection tier forever.
|
|
392
467
|
*
|
|
393
468
|
* @param {object} server - McpServer instance
|
|
394
469
|
* @param {Function} apiFn - The api() helper
|
|
395
|
-
* @param {object} auth - AUTH context
|
|
470
|
+
* @param {object|Function} auth - AUTH context, or a getter returning it
|
|
396
471
|
* @param {Function} checkToolAccessFn - checkToolAccess from auth.js
|
|
397
472
|
*/
|
|
398
473
|
export function wrapWithAnalytics(server, apiFn, auth, checkToolAccessFn) {
|
|
399
474
|
if (!TELEMETRY_ENABLED) return;
|
|
475
|
+
if (analyticsWrapperStates.has(server)) return;
|
|
476
|
+
const wrapperState = { countedToolNames: new Set() };
|
|
477
|
+
analyticsWrapperStates.set(server, wrapperState);
|
|
400
478
|
|
|
479
|
+
const resolveAuth = () => (typeof auth === 'function' ? auth() : auth);
|
|
401
480
|
const _analyticsWrappedTool = server.tool.bind(server);
|
|
402
481
|
|
|
403
482
|
server.tool = function analyticsTool(name, ...rest) {
|
|
404
483
|
const handler = rest[rest.length - 1];
|
|
484
|
+
wrapperState.countedToolNames.add(name);
|
|
405
485
|
rest[rest.length - 1] = async function analyticsHandler(...handlerArgs) {
|
|
406
486
|
const start = Date.now();
|
|
407
487
|
let success = true;
|
|
408
488
|
let errorMsg = null;
|
|
489
|
+
const auth = resolveAuth();
|
|
409
490
|
|
|
410
491
|
// Track tier gate if access was denied (check without modifying flow)
|
|
411
492
|
const access = checkToolAccessFn(name, auth);
|
|
@@ -574,16 +655,18 @@ function parsePeriod(period) {
|
|
|
574
655
|
* @param {object} opts - { z, api, auth, ok, checkToolAccess }
|
|
575
656
|
*/
|
|
576
657
|
export function registerAnalytics(server, { z, api, auth, ok, checkToolAccess }) {
|
|
658
|
+
const resolveAuth = () => (typeof auth === 'function' ? auth() : auth);
|
|
659
|
+
|
|
577
660
|
// Wire up the analytics wrapper on all future tool registrations
|
|
578
661
|
wrapWithAnalytics(server, api, auth, checkToolAccess);
|
|
579
662
|
|
|
580
663
|
// Start a session
|
|
581
|
-
startSession(api,
|
|
664
|
+
startSession(api, resolveAuth());
|
|
582
665
|
|
|
583
666
|
// Graceful shutdown: end session on process exit
|
|
584
|
-
process.on('beforeExit', () => endSession(api,
|
|
585
|
-
process.on('SIGINT', () => { endSession(api,
|
|
586
|
-
process.on('SIGTERM', () => { endSession(api,
|
|
667
|
+
process.on('beforeExit', () => endSession(api, resolveAuth()));
|
|
668
|
+
process.on('SIGINT', () => { endSession(api, resolveAuth()); process.exit(0); });
|
|
669
|
+
process.on('SIGTERM', () => { endSession(api, resolveAuth()); process.exit(0); });
|
|
587
670
|
|
|
588
671
|
// ----- get_mcp_analytics -----
|
|
589
672
|
server.tool('get_mcp_analytics',
|
|
@@ -594,27 +677,65 @@ export function registerAnalytics(server, { z, api, auth, ok, checkToolAccess })
|
|
|
594
677
|
async (args) => {
|
|
595
678
|
const period = args.period || '7d';
|
|
596
679
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
680
|
+
// Coverage travels with every answer. `tool_invocations.total: 0` from a
|
|
681
|
+
// fully instrumented surface means nobody called anything; the same zero
|
|
682
|
+
// from a surface the wrapper never reached means nothing was measured.
|
|
683
|
+
// Reporting the second as the first is how this counter read 0 for 227
|
|
684
|
+
// of 254 tools without anyone noticing.
|
|
685
|
+
const coverage = getAnalyticsCoverage(server);
|
|
686
|
+
|
|
687
|
+
if (resolveAuth().authenticated) {
|
|
688
|
+
// Authenticated hosted sessions persist invocation rows in the API,
|
|
689
|
+
// not ~/.adrata. Reading the local dashboard here used to both report
|
|
690
|
+
// zero and attempt to mkdir /home/adrata/.adrata in the read-only ECS
|
|
691
|
+
// image. The profile endpoint is the canonical named-tool rollup.
|
|
601
692
|
try {
|
|
602
|
-
const
|
|
603
|
-
const
|
|
693
|
+
const profileData = await api('GET', '/api/v1/mcp/profile');
|
|
694
|
+
const profile = profileData?.data || profileData || {};
|
|
695
|
+
const stats = profile.stats || {};
|
|
696
|
+
const topTools = Array.isArray(stats.top_tools)
|
|
697
|
+
? stats.top_tools.map((entry) => ({
|
|
698
|
+
name: entry.name || entry.tool,
|
|
699
|
+
count: entry.count,
|
|
700
|
+
}))
|
|
701
|
+
: [];
|
|
702
|
+
|
|
703
|
+
let usage = null;
|
|
704
|
+
try {
|
|
705
|
+
const subscriptionData = await api('GET', '/api/v1/mcp/subscription');
|
|
706
|
+
const subscription = subscriptionData?.data || subscriptionData || {};
|
|
707
|
+
usage = subscription.usage || null;
|
|
708
|
+
} catch {
|
|
709
|
+
// Named invocation analytics remain valid when billing is absent.
|
|
710
|
+
}
|
|
711
|
+
|
|
604
712
|
return ok({
|
|
605
713
|
period,
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
714
|
+
coverage,
|
|
715
|
+
source: 'server',
|
|
716
|
+
server_scope: 'all_time',
|
|
717
|
+
period_note: 'The server profile currently reports all-time named invocation totals; period applies only to the local fallback.',
|
|
718
|
+
tool_invocations: {
|
|
719
|
+
total: Number(stats.total_queries || 0),
|
|
720
|
+
success: null,
|
|
721
|
+
errors: null,
|
|
722
|
+
avg_latency_ms: null,
|
|
723
|
+
top_tools: topTools,
|
|
724
|
+
},
|
|
725
|
+
server: profile,
|
|
726
|
+
usage,
|
|
727
|
+
server_events_30d: usage ? usage.tool_invocations_30d : null,
|
|
609
728
|
});
|
|
610
729
|
} catch {
|
|
611
|
-
//
|
|
612
|
-
|
|
730
|
+
// Offline-compatible fallback. readLocalAnalytics does not create a
|
|
731
|
+
// directory merely to answer an empty read, so this cannot break a
|
|
732
|
+
// read-only hosted container either.
|
|
733
|
+
return ok({ ...getLocalDashboard(period), coverage, source: 'local_fallback' });
|
|
613
734
|
}
|
|
614
735
|
}
|
|
615
736
|
|
|
616
737
|
// Free tier: local analytics only
|
|
617
|
-
return ok(getLocalDashboard(period));
|
|
738
|
+
return ok({ ...getLocalDashboard(period), coverage });
|
|
618
739
|
}
|
|
619
740
|
);
|
|
620
741
|
|
|
@@ -627,7 +748,7 @@ export function registerAnalytics(server, { z, api, auth, ok, checkToolAccess })
|
|
|
627
748
|
trigger: z.string().optional().describe('What triggered the conversion'),
|
|
628
749
|
},
|
|
629
750
|
async (args) => {
|
|
630
|
-
trackConversion(api,
|
|
751
|
+
trackConversion(api, resolveAuth(), args.from_tier, args.to_tier, args.trigger || 'manual');
|
|
631
752
|
return ok({ tracked: true, from: args.from_tier, to: args.to_tier });
|
|
632
753
|
}
|
|
633
754
|
);
|
package/edge-block.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tell an edge block apart from an authorization failure.
|
|
3
|
+
*
|
|
4
|
+
* The problem this exists for. api.adrata.com resolves to the ALB directly, and
|
|
5
|
+
* an AWS WAF web ACL sits on that ALB. When a managed rule matches a REQUEST
|
|
6
|
+
* BODY the request is refused at the edge and never reaches the Rust API, so
|
|
7
|
+
* the caller gets a bare HTML page:
|
|
8
|
+
*
|
|
9
|
+
* <html>
|
|
10
|
+
* <head><title>403 Forbidden</title></head>
|
|
11
|
+
* <body>
|
|
12
|
+
* <center><h1>403 Forbidden</h1></center>
|
|
13
|
+
* </body>
|
|
14
|
+
* </html>
|
|
15
|
+
*
|
|
16
|
+
* 403 is also what the API returns for a genuine `insufficient_scope`. Nothing
|
|
17
|
+
* in the old error message distinguished them, so the reasonable conclusion was
|
|
18
|
+
* "my token lost board write access" — and the reasonable next step was to ask
|
|
19
|
+
* the owner to reconnect the workspace. That reconnect loop has been run more
|
|
20
|
+
* than once for a cause that had nothing to do with credentials.
|
|
21
|
+
*
|
|
22
|
+
* The tell is decisive and needs no guessing. The Adrata API serialises EVERY
|
|
23
|
+
* error through one `IntoResponse` impl that always emits a JSON envelope
|
|
24
|
+
* (`code/api/crates/core/src/error.rs`), and its fallback route is JSON-only.
|
|
25
|
+
* An HTML body from this host is therefore structurally impossible to have come
|
|
26
|
+
* from the application. Something in front of it answered.
|
|
27
|
+
*
|
|
28
|
+
* Measured 2026-08-29 against production, same endpoint, same session, same
|
|
29
|
+
* everything but the body text:
|
|
30
|
+
*
|
|
31
|
+
* plain prose ....................... 404 JSON (reached the app)
|
|
32
|
+
* markdown table, pipes only ........ 404 JSON (reached the app)
|
|
33
|
+
* fenced code block, backticks ...... 404 JSON (reached the app)
|
|
34
|
+
* JSX with angle brackets ........... 404 JSON (reached the app)
|
|
35
|
+
* "aaa ../ bbb" ..................... 403 HTML (refused at the edge)
|
|
36
|
+
* "link:../../packages/..." ......... 403 HTML (refused at the edge)
|
|
37
|
+
* "the file /etc/passwd is not read" 404 JSON (reached the app)
|
|
38
|
+
*
|
|
39
|
+
* So the trigger is the path-traversal token `../`, not markdown structure —
|
|
40
|
+
* which matters, because "avoid tables and code fences" is the wrong lesson and
|
|
41
|
+
* would quietly strip evidence out of bug reports for no reason.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** Substrings measured to be refused at the edge, most specific first. */
|
|
45
|
+
const MEASURED_TRIGGERS = [
|
|
46
|
+
{ pattern: '../', label: 'a relative path segment (`../`)' },
|
|
47
|
+
{ pattern: '..\\', label: 'a Windows relative path segment (`..\\`)' },
|
|
48
|
+
{ pattern: '..%2f', label: 'a percent-encoded path segment (`..%2f`)' },
|
|
49
|
+
{ pattern: '..%5c', label: 'a percent-encoded path segment (`..%5c`)' },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* True when a response body cannot have come from the Adrata API.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately narrow. It looks for an HTML document rather than for "not
|
|
56
|
+
* JSON": an empty body, a timeout, or a truncated read are different failures
|
|
57
|
+
* and must not be reported as an edge block.
|
|
58
|
+
*/
|
|
59
|
+
export function looksLikeEdgeHtml(text) {
|
|
60
|
+
if (typeof text !== 'string') return false;
|
|
61
|
+
const head = text.trimStart().slice(0, 400).toLowerCase();
|
|
62
|
+
if (!head.startsWith('<html') && !head.startsWith('<!doctype html')) return false;
|
|
63
|
+
return head.includes('<title>') || head.includes('<h1>');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Name the content the edge is most likely to have objected to.
|
|
68
|
+
*
|
|
69
|
+
* Returns every measured trigger present, because a body often carries more
|
|
70
|
+
* than one and fixing only the first sends the caller round again. Unknown is
|
|
71
|
+
* reported as unknown: a body with no measured trigger returns an empty list
|
|
72
|
+
* rather than a guess, so the caller is never told to edit the wrong sentence.
|
|
73
|
+
*/
|
|
74
|
+
export function offendingContent(requestBody) {
|
|
75
|
+
if (requestBody == null) return [];
|
|
76
|
+
let serialized;
|
|
77
|
+
try {
|
|
78
|
+
serialized = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody);
|
|
79
|
+
} catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
if (typeof serialized !== 'string') return [];
|
|
83
|
+
const haystack = serialized.toLowerCase();
|
|
84
|
+
return MEASURED_TRIGGERS.filter(({ pattern }) => haystack.includes(pattern)).map(
|
|
85
|
+
({ label }) => label
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build the message for a request the edge refused, or return null when this
|
|
91
|
+
* was an ordinary API error and the caller should report it unchanged.
|
|
92
|
+
*/
|
|
93
|
+
export function describeEdgeBlock({ status, text, method, path, requestBody } = {}) {
|
|
94
|
+
if (!looksLikeEdgeHtml(text)) return null;
|
|
95
|
+
if (typeof status !== 'number' || status < 400 || status > 499) return null;
|
|
96
|
+
|
|
97
|
+
const found = offendingContent(requestBody);
|
|
98
|
+
const because = found.length
|
|
99
|
+
? `The request body contains ${found.join(' and ')}, which the edge's path-traversal rule matches.`
|
|
100
|
+
: 'No known trigger was found in this request body, so the matching rule is not one already measured. Report the request id and the body, and check the WAF sampled requests.';
|
|
101
|
+
|
|
102
|
+
return [
|
|
103
|
+
`API ${method} ${path} → ${status}, refused at the network edge before it reached Adrata.`,
|
|
104
|
+
'',
|
|
105
|
+
'This is NOT an authentication or scope failure, and reconnecting the workspace will not',
|
|
106
|
+
'change it. The proof is the response body: the Adrata API serialises every error as JSON,',
|
|
107
|
+
'so an HTML error page from this host was produced by something in front of it (an AWS WAF',
|
|
108
|
+
'managed rule on the ALB). Your session is unaffected — do not call connect_workspace, and',
|
|
109
|
+
'do not ask the owner to reconnect.',
|
|
110
|
+
'',
|
|
111
|
+
because,
|
|
112
|
+
'',
|
|
113
|
+
'What to do: rewrite the offending text and resend. A relative path reads the same as',
|
|
114
|
+
'`code/desktop` or `<repo root>/packages/client-runtime`, and the record is no worse for it.',
|
|
115
|
+
'Do NOT strip out markdown tables or fenced code blocks — those were measured to pass, and',
|
|
116
|
+
'removing them only makes the record vaguer.',
|
|
117
|
+
].join('\n');
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adrata/adrata-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Adrata MCP Server \u2014 connect Claude Code, Codex, Gemini, Cursor, and other AI tools to your CRM. 80+ tools for companies, people, deals, actions, buyer groups, warm intros, webhooks, intelligence, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node server.js",
|
|
12
|
-
"test": "node --test server.test.js api-bridge.test.js audit-flush.test.js buyer-group-writes.test.js note-writes.test.js mcp-spec.test.js packaging.test.js product-profile.test.js security.test.js tool-annotations.test.js toolsets.test.js access/auth.test.js access/oauth-callback.test.js access/oauth-session.test.js access/oauth-capabilities.test.js scripts/local-dev-server.test.js tools/email-tools.test.js tools/scheduling.test.js tools/work-board-tools.test.js tools/work-hub/audit.test.js tools/roadmap-tools.test.js governance/money.test.js"
|
|
12
|
+
"test": "node --test analytics.test.js server.test.js api-bridge.test.js edge-block.test.js audit-flush.test.js buyer-group-writes.test.js note-writes.test.js mcp-spec.test.js packaging.test.js product-profile.test.js security.test.js tool-annotations.test.js toolsets.test.js access/auth.test.js access/oauth-callback.test.js access/oauth-session.test.js access/oauth-capabilities.test.js scripts/local-dev-server.test.js tools/competitive-coverage.test.js tools/email-tools.test.js tools/scheduling.test.js tools/work-board-tools.test.js tools/work-hub/audit.test.js tools/roadmap-tools.test.js tools/source-control/connection-tools.test.js governance/money.test.js"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"mcp",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"api-bridge.js",
|
|
45
45
|
"analytics.js",
|
|
46
46
|
"security.js",
|
|
47
|
+
"edge-block.js",
|
|
47
48
|
"transport-http.js",
|
|
48
49
|
"resources.js",
|
|
49
50
|
"tool-annotations.js",
|