@zackbart/connecta 0.12.2 → 0.13.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/CHANGELOG.md +137 -0
- package/README.md +4 -1
- package/dist/catalog-service.d.ts +41 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +94 -5
- package/dist/catalog-service.js.map +1 -1
- package/dist/connectors/api.d.ts +5 -4
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +5 -4
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +12 -10
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +5 -4
- package/dist/meta-tools.js.map +1 -1
- package/dist/providers/mixpanel.d.ts +21 -0
- package/dist/providers/mixpanel.d.ts.map +1 -0
- package/dist/providers/mixpanel.js +183 -0
- package/dist/providers/mixpanel.js.map +1 -0
- package/dist/skills.d.ts +7 -9
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +58 -24
- package/dist/skills.js.map +1 -1
- package/dist/types.d.ts +26 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/code-mode.md +6 -6
- package/documentation/connectors.md +116 -4
- package/documentation/meta-tools.md +80 -8
- package/documentation/mixpanel.md +72 -0
- package/ethos.md +8 -3
- package/package.json +5 -1
- package/src/catalog-service.ts +139 -4
- package/src/connectors/api.ts +5 -3
- package/src/connectors/remote-mcp.ts +5 -3
- package/src/execute.ts +18 -10
- package/src/index.ts +1 -0
- package/src/meta-tools.ts +10 -4
- package/src/providers/mixpanel.ts +220 -0
- package/src/skills.ts +64 -23
- package/src/types.ts +27 -6
- package/src/version.ts +1 -1
- package/templates/node/package.json +1 -1
package/src/catalog-service.ts
CHANGED
|
@@ -25,6 +25,8 @@ import type {
|
|
|
25
25
|
} from "./registry.js";
|
|
26
26
|
import {
|
|
27
27
|
connectorGuide,
|
|
28
|
+
connectorGuideRequired,
|
|
29
|
+
connectorGuideSummary,
|
|
28
30
|
connectorSkillName,
|
|
29
31
|
} from "./skills.js";
|
|
30
32
|
import {
|
|
@@ -196,6 +198,7 @@ export interface CatalogDescribeArgs {
|
|
|
196
198
|
interface CatalogSearchEntry {
|
|
197
199
|
connector: Connector;
|
|
198
200
|
guide?: string;
|
|
201
|
+
guideSummary?: string;
|
|
199
202
|
tool: {
|
|
200
203
|
name: string;
|
|
201
204
|
address: string;
|
|
@@ -208,9 +211,34 @@ interface CatalogSearchEntry {
|
|
|
208
211
|
requiredInputKeys?: string[];
|
|
209
212
|
outputKeys?: string[];
|
|
210
213
|
annotations?: ToolDef["annotations"];
|
|
214
|
+
guideRequired?: true;
|
|
215
|
+
guideRequiredReasons?: GuideRequiredReason[];
|
|
211
216
|
};
|
|
212
217
|
}
|
|
213
218
|
|
|
219
|
+
type GuideRequiredReason =
|
|
220
|
+
| "connector_required"
|
|
221
|
+
| "approval_required"
|
|
222
|
+
| "schema_truncated";
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Reasons discovery can determine without reading arguments or guessing at a
|
|
226
|
+
* task. Summary-only conventions remain an agent decision; hard requirements
|
|
227
|
+
* are explicit and machine-readable.
|
|
228
|
+
*/
|
|
229
|
+
function guideRequiredReasons(
|
|
230
|
+
connector: Connector,
|
|
231
|
+
tool: ToolDef,
|
|
232
|
+
schemaTruncated: boolean,
|
|
233
|
+
): GuideRequiredReason[] | undefined {
|
|
234
|
+
if (!connectorGuide(connector)) return undefined;
|
|
235
|
+
const reasons: GuideRequiredReason[] = [];
|
|
236
|
+
if (connectorGuideRequired(connector)) reasons.push("connector_required");
|
|
237
|
+
if (!isExplicitlyReadOnly(tool)) reasons.push("approval_required");
|
|
238
|
+
if (schemaTruncated) reasons.push("schema_truncated");
|
|
239
|
+
return reasons.length > 0 ? reasons : undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
214
242
|
/**
|
|
215
243
|
* Code-mode key metadata for one match. Each half is omitted when its schema
|
|
216
244
|
* does not resolve to an object shape, so a program reads "no metadata, use the
|
|
@@ -237,6 +265,19 @@ function schemaKeyMetadata(
|
|
|
237
265
|
};
|
|
238
266
|
}
|
|
239
267
|
|
|
268
|
+
/**
|
|
269
|
+
* The classified-failure subset a scoped search may echo: enough to tell a
|
|
270
|
+
* transient outage from one an operator must clear, and nothing more. Kept as
|
|
271
|
+
* its own type rather than `CallErrorDetails` so widening the call-path
|
|
272
|
+
* classifier cannot widen this discovery-surface field by accident.
|
|
273
|
+
*/
|
|
274
|
+
interface CatalogFailureDetail {
|
|
275
|
+
code: string;
|
|
276
|
+
message: string;
|
|
277
|
+
retryable: boolean;
|
|
278
|
+
retryAfterMs?: number;
|
|
279
|
+
}
|
|
280
|
+
|
|
240
281
|
export interface CatalogSearchPage {
|
|
241
282
|
entries: CatalogSearchEntry[];
|
|
242
283
|
total: number;
|
|
@@ -253,6 +294,12 @@ export interface CatalogSearchPage {
|
|
|
253
294
|
connectorScope?: string;
|
|
254
295
|
unknownConnector?: true;
|
|
255
296
|
unavailableConnectorCount?: number;
|
|
297
|
+
/** Bounded typed failure for an explicitly scoped unavailable catalog. */
|
|
298
|
+
catalogError?: CatalogFailureDetail;
|
|
299
|
+
guide?: string;
|
|
300
|
+
guideSummary?: string;
|
|
301
|
+
guideRequired?: true;
|
|
302
|
+
guideRequiredReasons?: GuideRequiredReason[];
|
|
256
303
|
guidance?: string;
|
|
257
304
|
};
|
|
258
305
|
}
|
|
@@ -262,6 +309,9 @@ export interface CatalogDescription {
|
|
|
262
309
|
name?: string;
|
|
263
310
|
description?: string;
|
|
264
311
|
guide?: string;
|
|
312
|
+
guideSummary?: string;
|
|
313
|
+
guideRequired?: true;
|
|
314
|
+
guideRequiredReasons?: GuideRequiredReason[];
|
|
265
315
|
inputSchema?: unknown;
|
|
266
316
|
outputSchema?: unknown;
|
|
267
317
|
annotations?: ToolDef["annotations"];
|
|
@@ -656,10 +706,19 @@ export class CatalogService {
|
|
|
656
706
|
match.tool.description,
|
|
657
707
|
args.fullDescriptions === true,
|
|
658
708
|
);
|
|
709
|
+
const requiredReasons = guideRequiredReasons(
|
|
710
|
+
match.connector,
|
|
711
|
+
match.tool,
|
|
712
|
+
renderedInput?.truncated === true || renderedOutput?.truncated === true,
|
|
713
|
+
);
|
|
714
|
+
const guideSummary = connectorGuideSummary(match.connector);
|
|
659
715
|
return {
|
|
660
716
|
connector: match.connector,
|
|
661
717
|
...(connectorGuide(match.connector)
|
|
662
|
-
? {
|
|
718
|
+
? {
|
|
719
|
+
guide: connectorSkillName(match.connector.id),
|
|
720
|
+
...(guideSummary ? { guideSummary } : {}),
|
|
721
|
+
}
|
|
663
722
|
: {}),
|
|
664
723
|
tool: {
|
|
665
724
|
name: match.tool.name,
|
|
@@ -697,6 +756,12 @@ export class CatalogService {
|
|
|
697
756
|
...(match.tool.annotations
|
|
698
757
|
? { annotations: match.tool.annotations }
|
|
699
758
|
: {}),
|
|
759
|
+
...(requiredReasons
|
|
760
|
+
? {
|
|
761
|
+
guideRequired: true as const,
|
|
762
|
+
guideRequiredReasons: requiredReasons,
|
|
763
|
+
}
|
|
764
|
+
: {}),
|
|
700
765
|
},
|
|
701
766
|
};
|
|
702
767
|
});
|
|
@@ -732,6 +797,27 @@ export class CatalogService {
|
|
|
732
797
|
const unavailableCatalogs = catalogs.filter(
|
|
733
798
|
(catalog) => catalog.status === "rejected",
|
|
734
799
|
).length;
|
|
800
|
+
// Named field by field rather than spread: `CallErrorDetails` also carries
|
|
801
|
+
// connector, operation, recovery, and nextAction, and a discovery read is
|
|
802
|
+
// not a call — widening the classifier must not silently widen what a
|
|
803
|
+
// catalog search hands back.
|
|
804
|
+
const scopedCatalogError = ((): CatalogFailureDetail | undefined => {
|
|
805
|
+
if (!scopedConnector || catalogs[0]?.status !== "rejected") {
|
|
806
|
+
return undefined;
|
|
807
|
+
}
|
|
808
|
+
const error = classifyCallError(
|
|
809
|
+
catalogs[0].reason,
|
|
810
|
+
"catalog_lookup_failed",
|
|
811
|
+
);
|
|
812
|
+
return {
|
|
813
|
+
code: error.code,
|
|
814
|
+
message: boundedEchoText(error.message),
|
|
815
|
+
retryable: error.retryable,
|
|
816
|
+
...(error.retryAfterMs === undefined
|
|
817
|
+
? {}
|
|
818
|
+
: { retryAfterMs: error.retryAfterMs }),
|
|
819
|
+
};
|
|
820
|
+
})();
|
|
735
821
|
const safetyLabel =
|
|
736
822
|
safety === "readOnly"
|
|
737
823
|
? "read-only "
|
|
@@ -740,6 +826,14 @@ export class CatalogService {
|
|
|
740
826
|
: "";
|
|
741
827
|
const filterRecovery =
|
|
742
828
|
safety === "all" ? "" : " Change safety to inspect the other tools.";
|
|
829
|
+
const scopedGuide =
|
|
830
|
+
matches.length === 0 && scopedConnector && connectorGuide(scopedConnector)
|
|
831
|
+
? {
|
|
832
|
+
guide: connectorSkillName(scopedConnector.id),
|
|
833
|
+
guideSummary: connectorGuideSummary(scopedConnector),
|
|
834
|
+
required: connectorGuideRequired(scopedConnector),
|
|
835
|
+
}
|
|
836
|
+
: undefined;
|
|
743
837
|
const guidance =
|
|
744
838
|
queryTerms.length === 0
|
|
745
839
|
? undefined
|
|
@@ -748,8 +842,10 @@ export class CatalogService {
|
|
|
748
842
|
? `Connector "${args.connector}" is not configured in this deployment. Omit connector to search all configured tools.`
|
|
749
843
|
: scopedConnector
|
|
750
844
|
? unavailableCatalogs > 0
|
|
751
|
-
? `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable.
|
|
752
|
-
:
|
|
845
|
+
? `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
|
|
846
|
+
: scopedGuide?.required
|
|
847
|
+
? `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Fetch queryAnalysis.guide before calling, then refine terms or browse with an empty query.${filterRecovery}`
|
|
848
|
+
: `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Refine terms or browse it with an empty query.${filterRecovery}`
|
|
753
849
|
: unavailableCatalogs === 0
|
|
754
850
|
? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
|
|
755
851
|
: `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
|
|
@@ -789,6 +885,23 @@ export class CatalogService {
|
|
|
789
885
|
...(unavailableCatalogs > 0
|
|
790
886
|
? { unavailableConnectorCount: unavailableCatalogs }
|
|
791
887
|
: {}),
|
|
888
|
+
...(scopedCatalogError ? { catalogError: scopedCatalogError } : {}),
|
|
889
|
+
...(scopedGuide
|
|
890
|
+
? {
|
|
891
|
+
guide: scopedGuide.guide,
|
|
892
|
+
...(scopedGuide.guideSummary
|
|
893
|
+
? { guideSummary: scopedGuide.guideSummary }
|
|
894
|
+
: {}),
|
|
895
|
+
...(scopedGuide.required
|
|
896
|
+
? {
|
|
897
|
+
guideRequired: true as const,
|
|
898
|
+
guideRequiredReasons: [
|
|
899
|
+
"connector_required" as const,
|
|
900
|
+
],
|
|
901
|
+
}
|
|
902
|
+
: {}),
|
|
903
|
+
}
|
|
904
|
+
: {}),
|
|
792
905
|
...(guidance ? { guidance } : {}),
|
|
793
906
|
},
|
|
794
907
|
}
|
|
@@ -853,12 +966,27 @@ export class CatalogService {
|
|
|
853
966
|
tool.description,
|
|
854
967
|
args.fullDescriptions === true,
|
|
855
968
|
);
|
|
969
|
+
const requiredReasons = guideRequiredReasons(
|
|
970
|
+
addressResolution.connector,
|
|
971
|
+
tool,
|
|
972
|
+
false,
|
|
973
|
+
);
|
|
974
|
+
const guideSummary = connectorGuideSummary(addressResolution.connector);
|
|
856
975
|
return {
|
|
857
976
|
address,
|
|
858
977
|
name: tool.name,
|
|
859
978
|
...(description !== undefined ? { description } : {}),
|
|
860
979
|
...(connectorGuide(addressResolution.connector)
|
|
861
|
-
? {
|
|
980
|
+
? {
|
|
981
|
+
guide: connectorSkillName(addressResolution.connector.id),
|
|
982
|
+
...(guideSummary ? { guideSummary } : {}),
|
|
983
|
+
}
|
|
984
|
+
: {}),
|
|
985
|
+
...(requiredReasons
|
|
986
|
+
? {
|
|
987
|
+
guideRequired: true as const,
|
|
988
|
+
guideRequiredReasons: requiredReasons,
|
|
989
|
+
}
|
|
862
990
|
: {}),
|
|
863
991
|
inputSchema: renderSchema(input, format),
|
|
864
992
|
...(tool.outputSchema
|
|
@@ -877,6 +1005,7 @@ export function groupedSearchResult(page: CatalogSearchPage) {
|
|
|
877
1005
|
id: string;
|
|
878
1006
|
title?: string;
|
|
879
1007
|
guide?: string;
|
|
1008
|
+
guideSummary?: string;
|
|
880
1009
|
tools: CatalogSearchEntry["tool"][];
|
|
881
1010
|
}> = [];
|
|
882
1011
|
const byConnector = new Map<string, (typeof groups)[number]>();
|
|
@@ -889,6 +1018,9 @@ export function groupedSearchResult(page: CatalogSearchPage) {
|
|
|
889
1018
|
id: entry.connector.id,
|
|
890
1019
|
...(entry.connector.title ? { title: entry.connector.title } : {}),
|
|
891
1020
|
...(entry.guide ? { guide: entry.guide } : {}),
|
|
1021
|
+
...(entry.guideSummary
|
|
1022
|
+
? { guideSummary: entry.guideSummary }
|
|
1023
|
+
: {}),
|
|
892
1024
|
tools: [],
|
|
893
1025
|
};
|
|
894
1026
|
byConnector.set(entry.connector.id, group);
|
|
@@ -913,6 +1045,9 @@ export function flatSearchResult(page: CatalogSearchPage) {
|
|
|
913
1045
|
tools: page.entries.map((entry) => ({
|
|
914
1046
|
...entry.tool,
|
|
915
1047
|
...(entry.guide ? { guide: entry.guide } : {}),
|
|
1048
|
+
...(entry.guideSummary
|
|
1049
|
+
? { guideSummary: entry.guideSummary }
|
|
1050
|
+
: {}),
|
|
916
1051
|
})),
|
|
917
1052
|
total: page.total,
|
|
918
1053
|
offset: page.offset,
|
package/src/connectors/api.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ConnectorCredentialConfig,
|
|
6
6
|
ConnectorCredentialValues,
|
|
7
7
|
ConnectorContext,
|
|
8
|
+
ConnectorUsageGuide,
|
|
8
9
|
CredentialTestResult,
|
|
9
10
|
JsonSchema,
|
|
10
11
|
ToolAnnotations,
|
|
@@ -41,10 +42,11 @@ export interface ApiOptions {
|
|
|
41
42
|
/** Optional per-runtime downstream call-admission policy. */
|
|
42
43
|
callAdmission?: ConnectorCallAdmissionPolicy;
|
|
43
44
|
/**
|
|
44
|
-
* Optional agent-facing usage guide
|
|
45
|
-
*
|
|
45
|
+
* Optional agent-facing usage guide served by `skills` as
|
|
46
|
+
* `connector:<id>`. A string is markdown; the structured form adds bounded
|
|
47
|
+
* discovery metadata. See `Connector.usageGuide`.
|
|
46
48
|
*/
|
|
47
|
-
usageGuide?: string;
|
|
49
|
+
usageGuide?: string | ConnectorUsageGuide;
|
|
48
50
|
/** Optional operator-managed credential exposed through ctx.credential and /credentials. */
|
|
49
51
|
credential?: ConnectorCredentialConfig;
|
|
50
52
|
/** Optional validation behind /credentials' Test action. */
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
ConnectorCallAdmissionPolicy,
|
|
22
22
|
ConnectorContext,
|
|
23
23
|
ConnectorStatus,
|
|
24
|
+
ConnectorUsageGuide,
|
|
24
25
|
Logger,
|
|
25
26
|
ToolDef,
|
|
26
27
|
} from "../types.js";
|
|
@@ -47,10 +48,11 @@ export interface RemoteMcpOptions {
|
|
|
47
48
|
/** Optional per-runtime downstream call-admission policy. */
|
|
48
49
|
callAdmission?: ConnectorCallAdmissionPolicy;
|
|
49
50
|
/**
|
|
50
|
-
* Optional agent-facing usage guide
|
|
51
|
-
*
|
|
51
|
+
* Optional agent-facing usage guide served by `skills` as
|
|
52
|
+
* `connector:<id>`. A string is markdown; the structured form adds bounded
|
|
53
|
+
* discovery metadata. See `Connector.usageGuide`.
|
|
52
54
|
*/
|
|
53
|
-
usageGuide?: string;
|
|
55
|
+
usageGuide?: string | ConnectorUsageGuide;
|
|
54
56
|
auth?: RemoteMcpAuth;
|
|
55
57
|
/**
|
|
56
58
|
* Downstream HTTP redirect policy. Defaults to `"none"`: every redirect is
|
package/src/execute.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
InvocationService,
|
|
29
29
|
} from "./invocation.js";
|
|
30
30
|
import type { RegistryView } from "./registry.js";
|
|
31
|
+
import { hasConnectorGuides } from "./skills.js";
|
|
31
32
|
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
32
33
|
import type {
|
|
33
34
|
Executor,
|
|
@@ -1272,20 +1273,22 @@ function discardedEmitsText(emitted: EmitCollector): string {
|
|
|
1272
1273
|
|
|
1273
1274
|
const executeDescription = (
|
|
1274
1275
|
emitBudgets: { maxBytes: number; maxBlocks: number },
|
|
1275
|
-
|
|
1276
|
+
connectorGuides: boolean,
|
|
1277
|
+
) => `Choose the route before discovery. Exactly one unknown-address read uses top-level search_tools then call_tool; a known address uses call_tool directly. This is the primary surface for everything wider. If any result will be reduced — even from one connector call — or work has dependent/multiple calls, loops, joins, or branches, make exactly one execute_code call that searches, selects, calls, and reduces before returning. A discovery-only program wastes its round trip: finish here, don't return catalog matches for a later call. Only readOnlyHint: true tools are available. Limits: ${EXECUTE_MAX_HOST_CALLS} host calls per run, ${EXECUTE_MAX_BATCH_CALLS} per batch, ${EXECUTE_HOST_CALL_TIMEOUT_MS / 1_000}-second host deadline.
|
|
1276
1278
|
|
|
1277
1279
|
Write an async arrow function. It runs with NO network, filesystem, timers, or imports — the only capabilities are:
|
|
1278
|
-
-
|
|
1280
|
+
- Connector globals call <connectorId>.<toolName>(args) with one schema-matching args object. Sanitization: non-[A-Za-z0-9_$] → "_" (my-service.get.thing → my_service.get_thing), leading digit → "_" prefix, reserved word → "_" suffix.
|
|
1279
1281
|
- connecta.call(address, args) and connecta.batch(calls) — call raw addresses. Every batch entry is { address, ok: true, data } or { address, ok: false, error, errorDetails: { code, retryable } }; destructure that, not a bare result.
|
|
1280
|
-
- connecta.search(args) and connecta.describe
|
|
1281
|
-
- connecta.emit(block) —
|
|
1282
|
-
- connecta.ui(html, options?) —
|
|
1282
|
+
- connecta.search(args) loads catalogs and must be followed by selection and calls in this program; set connector to the obvious id to load one, otherwise it loads all. For distinct operations, make separate short searches here. Require address/description to match the operation, then check requiredInputKeys, truncation, safety, and outputs; never take the first lexical or merely input-compatible match. Choose the best compatible match; do not require it to be the only match. Missing outputKeys means inspect outputSchema, not discard the candidate. Compatible means every required key has a task/prior-result value; do not prefer zero required keys. Put every requiredInputKey in call args. For dependencies, match an earlier outputKey to the later requiredInputKey. Use displayed names; [] means no required keys, not permission to invent args. Describe only a truncated/insufficient compact shape. Reducers use declared outputKeys, never guessed items/results roots. connecta.describe takes { address: "<connectorId>.<toolName>" } or { addresses: [...] }. Use safety: "readOnly" to avoid advertising calls this sandbox cannot execute; it changes results, not authority. A missing key list means a non-object shape, not no fields — read the schema.${connectorGuides ? " A match with guideRequired: true is a hard stop: do not call it; describing the exact schema clears only a schema_truncated reason, so for any other reason return the exact guide name, fetch that guide with the top-level skills tool, then write the informed call." : ""}
|
|
1283
|
+
- connecta.emit(block) — emit exactly { type: "text", text } or { type: "image" | "audio", data (base64), mimeType }. Success-only, no host call, ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes; invalid or over-budget throws before accepting.
|
|
1284
|
+
- connecta.ui(html, options?) — one success-only view. One arg is display-only; live reads use { reads: { name: { address, fixedArgs?, viewArgs? } } }, then markup calls connecta.read(name, args). Read admission is enforced; fixed keys cannot be overridden and undeclared keys fail. It shares the ${emitBudgets.maxBytes}-byte emit budget — one budget, not two; a second, over-budget, or invalid call throws catchably. Bytes stay out of context, so the model reads the return value, not the view: return the initial summary from its variables; later reads update only the view.
|
|
1283
1285
|
- console.log(...) — captured and returned with the result.
|
|
1284
1286
|
|
|
1285
|
-
|
|
1287
|
+
Dependent example (only when the second call requires a value returned by the first): async () => { const { tools } = await connecta.search({ query: "pipeline run job logs", safety: "readOnly", includeSchemas: "compact" }); const pick = (suffix) => { const match = tools.find((t) => t.address.endsWith(suffix)); if (!match) throw new Error("no tool for " + suffix); return match.address; }; const run = await connecta.call(pick(".get_run"), { runId: 42 }); const logs = await connecta.call(pick(".get_job_logs"), { jobId: run.failedJobId }); return [run, logs]; }
|
|
1286
1288
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
+
Calls return plain values (JSON-parsing MCP text when possible) and throw; catch errors. A thrown error is only a message; connecta.batch tells a policy refusal from a transient failure. Never retry retryable: false or rate_limited immediately — there are no timers. Return JSON; large results truncate, so reduce instead of returning raw payloads.
|
|
1290
|
+
|
|
1291
|
+
Plain JS, no TypeScript. Compact schemas are TypeScript-like, not JSON Schema: write the property names they display; never guess positions or aliases.`;
|
|
1289
1292
|
|
|
1290
1293
|
/** Register the execute_code meta-tool. Only called when an executor is configured. */
|
|
1291
1294
|
export function registerExecuteTool(
|
|
@@ -1340,11 +1343,16 @@ export function registerExecuteTool(
|
|
|
1340
1343
|
server.registerTool(
|
|
1341
1344
|
"execute_code",
|
|
1342
1345
|
{
|
|
1343
|
-
description: executeDescription(
|
|
1346
|
+
description: executeDescription(
|
|
1347
|
+
emitBudgets,
|
|
1348
|
+
hasConnectorGuides(registry.listConnectors()),
|
|
1349
|
+
),
|
|
1344
1350
|
inputSchema: z.object({
|
|
1345
1351
|
code: z
|
|
1346
1352
|
.string()
|
|
1347
|
-
.describe(
|
|
1353
|
+
.describe(
|
|
1354
|
+
"One complete JavaScript async arrow function. Consume search/describe results and finish the task inside it; returning catalog data for a later call spends a round trip and buys nothing.",
|
|
1355
|
+
),
|
|
1348
1356
|
diagnostics: z
|
|
1349
1357
|
.boolean()
|
|
1350
1358
|
.optional()
|
package/src/index.ts
CHANGED
package/src/meta-tools.ts
CHANGED
|
@@ -1372,7 +1372,7 @@ export function createMetaTools(
|
|
|
1372
1372
|
};
|
|
1373
1373
|
}
|
|
1374
1374
|
|
|
1375
|
-
const SEARCH_DESC = `
|
|
1375
|
+
const SEARCH_DESC = `Use top-level search only for exactly one unreduced read, then call_tool, or for write-capable work, then call_destructive_tool. For read-only reduction, dependent or multiple calls, never search here: make one execute_code program that searches and calls. Use 2–4 distinctive action/object terms, not the full request; set connector to the obvious integration id to load one catalog instead of all; omit limit initially (default ${DEFAULT_SEARCH_LIMIT}), page to ${MAX_SEARCH_LIMIT} if needed. safety="readOnly" returns only calls available to call_tool/code; "approvalRequired" returns the rest; omitted/"all" returns all. This filters results, not authority. includeSchemas="compact" adds the input and any declared output shape, bounded; plain objects expose inputKeys, requiredInputKeys, and outputKeys; truncation flags mark incomplete shapes; matches also carry declared annotations. Require purpose/address fit plus compatible inputs, truncation, safety, and outputs — never the first lexical match. Empty query browses all.`;
|
|
1376
1376
|
const CALL_DESC =
|
|
1377
1377
|
'Use for ONE tool explicitly annotated readOnlyHint: true — the cheapest path for a single cold call. For two or more calls, dependent steps, loops, joins, or data reduction use execute_code, whose connecta.call and connecta.batch reach the same tools. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths; traverse arrays with [] (for example results[].id). Misses return data plus `$connecta` feedback. resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
|
|
1378
1378
|
const CALL_DESTRUCTIVE_DESC =
|
|
@@ -1396,9 +1396,11 @@ const SEARCH_WITH_DESCRIBE_DESC = `${SEARCH_DESC} Expand an ambiguous compact sh
|
|
|
1396
1396
|
*/
|
|
1397
1397
|
const GUIDE_NOTES = {
|
|
1398
1398
|
skills:
|
|
1399
|
-
|
|
1399
|
+
" skills({}) also lists this deployment's scoped connector guides; fetch only an exact name listed there or carried by discovery, never one inferred from a connector id.",
|
|
1400
1400
|
search:
|
|
1401
|
-
" A
|
|
1401
|
+
" A result carrying `guide` also carries a bounded `guideSummary`. `guideRequired: true` is a hard stop: fetch that exact guide before calling. `guideRequiredReasons` explains why — `connector_required` and `approval_required` stand however you expand the schema; `schema_truncated` clears once describe returns the exact one. Otherwise fetch only when the summary names a connector convention relevant to the task. A complete, unambiguous read-only schema needs no otherwise-irrelevant guide fetch.",
|
|
1402
|
+
destructive:
|
|
1403
|
+
" Before a consequential call, inspect the address through discovery or describe and fetch any connector guide it names.",
|
|
1402
1404
|
} as const;
|
|
1403
1405
|
|
|
1404
1406
|
/** `base`, plus its guide note when any VISIBLE connector carries a guide. */
|
|
@@ -1533,7 +1535,11 @@ export function registerMetaTools(
|
|
|
1533
1535
|
server.registerTool(
|
|
1534
1536
|
"call_destructive_tool",
|
|
1535
1537
|
{
|
|
1536
|
-
description:
|
|
1538
|
+
description: describedFor(
|
|
1539
|
+
registry,
|
|
1540
|
+
CALL_DESTRUCTIVE_DESC,
|
|
1541
|
+
"destructive",
|
|
1542
|
+
),
|
|
1537
1543
|
inputSchema: z.object({
|
|
1538
1544
|
...CALL_INPUT_SCHEMA,
|
|
1539
1545
|
// Bounded above, but with no lower bound: a model that sends `""` or
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import {
|
|
2
|
+
remoteMcp,
|
|
3
|
+
type RemoteMcpAuth,
|
|
4
|
+
} from "../connectors/remote-mcp.js";
|
|
5
|
+
import type {
|
|
6
|
+
Connector,
|
|
7
|
+
ConnectorCallAdmissionPolicy,
|
|
8
|
+
ToolDef,
|
|
9
|
+
} from "../types.js";
|
|
10
|
+
|
|
11
|
+
export type MixpanelRegion = "us" | "eu" | "in";
|
|
12
|
+
|
|
13
|
+
export const MIXPANEL_MCP_ENDPOINTS: Readonly<
|
|
14
|
+
Record<MixpanelRegion, string>
|
|
15
|
+
> = {
|
|
16
|
+
us: "https://mcp.mixpanel.com/mcp",
|
|
17
|
+
eu: "https://mcp-eu.mixpanel.com/mcp",
|
|
18
|
+
in: "https://mcp-in.mixpanel.com/mcp",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface MixpanelOptions {
|
|
22
|
+
/** Human-readable display name; defaults to "Mixpanel". */
|
|
23
|
+
title?: string;
|
|
24
|
+
/** Who should use this account and for what decisions. */
|
|
25
|
+
purpose: string;
|
|
26
|
+
/** Mixpanel data residency region. Defaults to "us". */
|
|
27
|
+
region?: MixpanelRegion;
|
|
28
|
+
/** OAuth by default; static headers support Mixpanel service accounts. */
|
|
29
|
+
auth?: RemoteMcpAuth;
|
|
30
|
+
/** Account-specific conventions appended to the maintained provider guide. */
|
|
31
|
+
instructions?: string;
|
|
32
|
+
/** Connector-specific inline result limit; omit to inherit the deployment. */
|
|
33
|
+
maxResultBytes?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Budget-only: a rejection computes its own retry-after from the window, and
|
|
37
|
+
// declaring `retryAfterMs` here would be a queue setting without a queue —
|
|
38
|
+
// which the admission controller refuses at construction.
|
|
39
|
+
const MIXPANEL_ADMISSION: ConnectorCallAdmissionPolicy = {
|
|
40
|
+
rules: [
|
|
41
|
+
{
|
|
42
|
+
budget: {
|
|
43
|
+
kind: "rolling-window",
|
|
44
|
+
maxCalls: 600,
|
|
45
|
+
windowMs: 3_600_000,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Tools whose official contract is observational rather than mutating. */
|
|
52
|
+
const READ_ONLY_TOOLS = new Set([
|
|
53
|
+
"Run-Query",
|
|
54
|
+
"Get-Query-Schema",
|
|
55
|
+
"Get-Report",
|
|
56
|
+
"Display-Query",
|
|
57
|
+
"List-Dashboards",
|
|
58
|
+
"Get-Dashboard",
|
|
59
|
+
"Get-Business-Context",
|
|
60
|
+
"Get-Projects",
|
|
61
|
+
"List-Organizations",
|
|
62
|
+
"Get-Events",
|
|
63
|
+
"List-Properties",
|
|
64
|
+
"Get-Property-Values",
|
|
65
|
+
"Search-Entities",
|
|
66
|
+
"Get-Issues",
|
|
67
|
+
"Get-Lexicon-URL",
|
|
68
|
+
"Find-Duplicate-Groups",
|
|
69
|
+
"Get-Custom-Property",
|
|
70
|
+
"Get-Cohort",
|
|
71
|
+
"List-Cohorts",
|
|
72
|
+
"Describe-Cohort-Schema",
|
|
73
|
+
"Get-Lookup-Table",
|
|
74
|
+
"Get-Metric",
|
|
75
|
+
"List-Metrics",
|
|
76
|
+
"Get-User-Replays-Data",
|
|
77
|
+
"List-Experiments",
|
|
78
|
+
"Get-Experiment",
|
|
79
|
+
"Get-Experiment-Setup-Guidance",
|
|
80
|
+
"Get-Experiment-Results-Interpretation-Guidance",
|
|
81
|
+
"Explain-Experiment-Health-Check",
|
|
82
|
+
"Run-Experiment-Pre-Launch-Checks",
|
|
83
|
+
"Search-Prior-Experiments",
|
|
84
|
+
"List-Feature-Flags",
|
|
85
|
+
"Get-Feature-Flag",
|
|
86
|
+
"Get-Feature-Flag-Setup-Guidance",
|
|
87
|
+
"Get-Feature-Flag-Lifecycle-Guidance",
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The maintained write catalog. `"destructive"` tools modify or remove state
|
|
92
|
+
* that already exists; `"additive"` ones only bring something new into being.
|
|
93
|
+
* Both leave the read-only path — the distinction only decides whether the
|
|
94
|
+
* connection asserts `destructiveHint`, which shapes the host's approval copy.
|
|
95
|
+
*/
|
|
96
|
+
const WRITE_TOOLS: ReadonlyMap<string, "additive" | "destructive"> = new Map([
|
|
97
|
+
["Create-Dashboard", "additive"],
|
|
98
|
+
["Update-Dashboard", "destructive"],
|
|
99
|
+
["Duplicate-Dashboard", "additive"],
|
|
100
|
+
["Delete-Dashboard", "destructive"],
|
|
101
|
+
["Edit-Event", "destructive"],
|
|
102
|
+
["Edit-Property", "destructive"],
|
|
103
|
+
["Bulk-Edit-Events", "destructive"],
|
|
104
|
+
["Bulk-Edit-Properties", "destructive"],
|
|
105
|
+
["Create-Tag", "additive"],
|
|
106
|
+
["Rename-Tag", "destructive"],
|
|
107
|
+
["Delete-Tag", "destructive"],
|
|
108
|
+
["Dismiss-Issues", "destructive"],
|
|
109
|
+
["Update-Business-Context", "destructive"],
|
|
110
|
+
["Dismiss-Duplicate-Group", "destructive"],
|
|
111
|
+
["Merge-Group", "destructive"],
|
|
112
|
+
["Create-Custom-Property", "additive"],
|
|
113
|
+
["Update-Custom-Property", "destructive"],
|
|
114
|
+
["Create-Cohort", "additive"],
|
|
115
|
+
["Update-Cohort", "destructive"],
|
|
116
|
+
["Delete-Cohort", "destructive"],
|
|
117
|
+
["Create-Lookup-Table", "additive"],
|
|
118
|
+
["Update-Lookup-Table", "destructive"],
|
|
119
|
+
["Create-Metric", "additive"],
|
|
120
|
+
["Update-Metric", "destructive"],
|
|
121
|
+
["Create-Experiment", "additive"],
|
|
122
|
+
["Update-Experiment", "destructive"],
|
|
123
|
+
["Create-Feature-Flag", "additive"],
|
|
124
|
+
["Update-Feature-Flag", "destructive"],
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fill in what the downstream leaves unsaid; never argue with what it says.
|
|
129
|
+
*
|
|
130
|
+
* A vetted classification may always tighten — that direction only ever routes
|
|
131
|
+
* more calls through `call_destructive_tool`. Loosening is the direction that
|
|
132
|
+
* needs the downstream's silence: an explicit `destructiveHint: true` or
|
|
133
|
+
* `readOnlyHint: false` on an allowlisted read name is the downstream telling
|
|
134
|
+
* us this release's allowlist is wrong, and it wins.
|
|
135
|
+
*/
|
|
136
|
+
function vettedSafety(definition: ToolDef): ToolDef {
|
|
137
|
+
const downstream = definition.annotations ?? {};
|
|
138
|
+
if (READ_ONLY_TOOLS.has(definition.name)) {
|
|
139
|
+
if (
|
|
140
|
+
downstream.destructiveHint === true ||
|
|
141
|
+
downstream.readOnlyHint === false
|
|
142
|
+
) {
|
|
143
|
+
return definition;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
...definition,
|
|
147
|
+
annotations: {
|
|
148
|
+
...downstream,
|
|
149
|
+
readOnlyHint: true,
|
|
150
|
+
destructiveHint: downstream.destructiveHint ?? false,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (WRITE_TOOLS.get(definition.name) === "destructive") {
|
|
155
|
+
return {
|
|
156
|
+
...definition,
|
|
157
|
+
annotations: {
|
|
158
|
+
...downstream,
|
|
159
|
+
readOnlyHint: false,
|
|
160
|
+
destructiveHint: true,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// Maintained additive creates and tools this release has never seen land
|
|
165
|
+
// here alike: not read-only, so the ordinary fail-closed path keeps them
|
|
166
|
+
// approval-visible, without claiming a create destroys anything.
|
|
167
|
+
return {
|
|
168
|
+
...definition,
|
|
169
|
+
annotations: {
|
|
170
|
+
...downstream,
|
|
171
|
+
readOnlyHint: false,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function usageGuide(purpose: string, instructions: string | undefined): string {
|
|
177
|
+
const accountInstructions = instructions?.trim();
|
|
178
|
+
return `# Mixpanel usage
|
|
179
|
+
|
|
180
|
+
Account purpose: ${purpose}
|
|
181
|
+
|
|
182
|
+
- Start with \`Get-Projects\`, then use \`Get-Business-Context\` for the selected project before interpreting its events or metrics.
|
|
183
|
+
- Discover names with \`Get-Events\`, \`List-Properties\`, and \`Get-Property-Values\`; do not guess event or property spelling.
|
|
184
|
+
- For a new analysis, fetch \`Get-Query-Schema\` before \`Run-Query\`. Reduce query results inside \`execute_code\` before returning them.
|
|
185
|
+
- Use \`Get-Report\` when the request names an existing saved report. Use \`Run-Query\` for a new question.
|
|
186
|
+
- Mixpanel limits MCP traffic to 600 requests per user per hour. Reuse discovery results within a run and avoid speculative fan-out.
|
|
187
|
+
- Treat every create, update, edit, merge, dismiss, duplicate, or delete operation as a write. Connecta routes the maintained write catalog through \`call_destructive_tool\`; newly added tools also fail closed until classified.
|
|
188
|
+
${
|
|
189
|
+
accountInstructions
|
|
190
|
+
? `\n## Account instructions\n\n${accountInstructions}\n`
|
|
191
|
+
: ""
|
|
192
|
+
}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** A maintained Mixpanel hosted-MCP connection. */
|
|
196
|
+
export function mixpanel(id: string, options: MixpanelOptions): Connector {
|
|
197
|
+
const purpose = options.purpose.trim();
|
|
198
|
+
if (!purpose) {
|
|
199
|
+
throw new Error("mixpanel() requires a non-empty account purpose.");
|
|
200
|
+
}
|
|
201
|
+
const region = options.region ?? "us";
|
|
202
|
+
const connector = remoteMcp(id, {
|
|
203
|
+
url: MIXPANEL_MCP_ENDPOINTS[region],
|
|
204
|
+
title: options.title ?? "Mixpanel",
|
|
205
|
+
description: `Mixpanel product analytics — ${purpose}`,
|
|
206
|
+
auth: options.auth ?? { type: "oauth" },
|
|
207
|
+
requireHttps: true,
|
|
208
|
+
callAdmission: MIXPANEL_ADMISSION,
|
|
209
|
+
usageGuide: usageGuide(purpose, options.instructions),
|
|
210
|
+
...(options.maxResultBytes !== undefined
|
|
211
|
+
? { maxResultBytes: options.maxResultBytes }
|
|
212
|
+
: {}),
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
...connector,
|
|
216
|
+
async listTools(ctx) {
|
|
217
|
+
return (await connector.listTools(ctx)).map(vettedSafety);
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|