@zackbart/connecta 0.12.2 → 0.14.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 +324 -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 +127 -11
- 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 +5 -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/cloudflare.d.ts +53 -0
- package/dist/providers/cloudflare.d.ts.map +1 -0
- package/dist/providers/cloudflare.js +1385 -0
- package/dist/providers/cloudflare.js.map +1 -0
- package/dist/providers/linear.d.ts +44 -0
- package/dist/providers/linear.d.ts.map +1 -0
- package/dist/providers/linear.js +243 -0
- package/dist/providers/linear.js.map +1 -0
- package/dist/providers/mixpanel.d.ts +21 -0
- package/dist/providers/mixpanel.d.ts.map +1 -0
- package/dist/providers/mixpanel.js +191 -0
- package/dist/providers/mixpanel.js.map +1 -0
- package/dist/providers/notion.d.ts +39 -0
- package/dist/providers/notion.d.ts.map +1 -0
- package/dist/providers/notion.js +1625 -0
- package/dist/providers/notion.js.map +1 -0
- package/dist/providers/stripe.d.ts +37 -0
- package/dist/providers/stripe.d.ts.map +1 -0
- package/dist/providers/stripe.js +232 -0
- package/dist/providers/stripe.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/cloudflare.md +268 -0
- package/documentation/code-mode.md +6 -6
- package/documentation/connectors.md +122 -4
- package/documentation/linear.md +144 -0
- package/documentation/meta-tools.md +91 -8
- package/documentation/mixpanel.md +77 -0
- package/documentation/notion.md +233 -0
- package/documentation/stripe.md +202 -0
- package/ethos.md +8 -3
- package/package.json +21 -1
- package/src/catalog-service.ts +174 -10
- package/src/connectors/api.ts +10 -4
- 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/cloudflare.ts +1696 -0
- package/src/providers/linear.ts +301 -0
- package/src/providers/mixpanel.ts +228 -0
- package/src/providers/notion.ts +1879 -0
- package/src/providers/stripe.ts +306 -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
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Stripe prebuilt connection
|
|
2
|
+
|
|
3
|
+
Import `stripe()` independently from `@zackbart/connecta/providers/stripe`. It
|
|
4
|
+
wraps [Stripe's hosted MCP server](https://docs.stripe.com/mcp) with a required
|
|
5
|
+
production/sandbox mode, OAuth by default, a mode-scaled admission policy, a
|
|
6
|
+
task-oriented usage guide, and a vetted safety classification. It adds no
|
|
7
|
+
provider dependency and is not reachable from Connecta's root entry.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { stripe } from "@zackbart/connecta/providers/stripe";
|
|
11
|
+
|
|
12
|
+
const billing = stripe("stripe_live", {
|
|
13
|
+
mode: "production",
|
|
14
|
+
title: "Stripe (production)",
|
|
15
|
+
purpose: "Revenue, disputes, and refunds for the real business",
|
|
16
|
+
instructions: "Never refund above $500 without a human in the loop.",
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The `id` owns the ordinary connector namespaces; use a different id for every
|
|
21
|
+
Stripe account. `purpose` is required because an agent choosing between two
|
|
22
|
+
instances needs to know which account answers the question. Account
|
|
23
|
+
`instructions` are appended to the maintained guide and cannot change the
|
|
24
|
+
connector's safety classification.
|
|
25
|
+
|
|
26
|
+
## Mode is required, and it is the whole point
|
|
27
|
+
|
|
28
|
+
`mode` accepts `"production"` or `"sandbox"` and has **no default**. There is no
|
|
29
|
+
safe guess between an account that moves real money and one that does not, so
|
|
30
|
+
the deployment has to say which it configured.
|
|
31
|
+
|
|
32
|
+
Stripe publishes exactly one endpoint — `https://mcp.stripe.com/` — and the
|
|
33
|
+
environment is selected by the credential, not the URL. Connecta therefore
|
|
34
|
+
cannot *route* by mode; what it can do is make the mode impossible for an agent
|
|
35
|
+
to miss, and refuse a deployment whose declaration and credential disagree.
|
|
36
|
+
|
|
37
|
+
`mode` shows up in four places an agent actually reads:
|
|
38
|
+
|
|
39
|
+
- the default `title` (`Stripe (production)` / `Stripe (sandbox)`);
|
|
40
|
+
- the `description`, which is what `search_tools` ranks and returns — production
|
|
41
|
+
reads `Stripe payments (production — live money and real customers) — …`,
|
|
42
|
+
sandbox reads `Stripe payments (sandbox — test data, no real money) — …`;
|
|
43
|
+
- the first two lines of the usage guide, which state the mode and then say
|
|
44
|
+
either "every write moves real money … a refund cannot be undone" or "never
|
|
45
|
+
answer a question about live revenue, payouts, or a named customer from this
|
|
46
|
+
connector";
|
|
47
|
+
- the admission policy, below.
|
|
48
|
+
|
|
49
|
+
And one place a deployment author reads: if `auth` is a `headers` credential
|
|
50
|
+
carrying a recognizable Stripe key prefix (`sk_`, `rk_`, or `pk_` with `_live_`
|
|
51
|
+
or `_test_`), construction throws when the key's mode contradicts the declared
|
|
52
|
+
one. That check reads nothing it cannot classify — an OAuth connector, or a
|
|
53
|
+
credential shape this release does not recognize, is left alone rather than
|
|
54
|
+
guessed at — and the error names only the two modes, never the key.
|
|
55
|
+
|
|
56
|
+
Deploy both side by side. Two instances are isolated exactly like two
|
|
57
|
+
hand-written connectors with different ids: separate addresses, catalogs,
|
|
58
|
+
credentials, storage, admission counters, and health.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
connectors: [
|
|
62
|
+
stripe("stripe_live", {
|
|
63
|
+
mode: "production",
|
|
64
|
+
purpose: "Revenue, disputes, and refunds for the real business",
|
|
65
|
+
}),
|
|
66
|
+
stripe("stripe_sandbox", {
|
|
67
|
+
mode: "sandbox",
|
|
68
|
+
purpose: "Rehearsing billing changes before they touch production",
|
|
69
|
+
}),
|
|
70
|
+
]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Authentication
|
|
74
|
+
|
|
75
|
+
OAuth is the default and the option Stripe recommends: it supports dynamic
|
|
76
|
+
client registration and PKCE, and each connector instance keeps its own flow
|
|
77
|
+
and tokens in connector-scoped storage. Stripe also accepts a
|
|
78
|
+
[restricted API key](https://docs.stripe.com/keys#create-restricted-api-key) as
|
|
79
|
+
a bearer token for headless agents:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
stripe("stripe_sandbox", {
|
|
83
|
+
mode: "sandbox",
|
|
84
|
+
purpose: "Automated billing rehearsal",
|
|
85
|
+
auth: {
|
|
86
|
+
type: "headers",
|
|
87
|
+
headers: { Authorization: `Bearer ${env.STRIPE_RESTRICTED_KEY}` },
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use a restricted key, not a secret key, and scope it to the operations the
|
|
93
|
+
agent actually needs; Stripe's own guidance is to "limit your agent's access to
|
|
94
|
+
exactly the functionality it requires". Keep it in the runtime's secret store.
|
|
95
|
+
|
|
96
|
+
Connect platforms can act as a connected account with `connectedAccount`, which
|
|
97
|
+
adds Stripe's `Stripe-Account` header. Stripe does not support OAuth for
|
|
98
|
+
connected-account calls, so this requires `headers` auth and throws otherwise:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
stripe("merchant_42", {
|
|
102
|
+
mode: "production",
|
|
103
|
+
purpose: "Billing questions for the merchant on account 42",
|
|
104
|
+
connectedAccount: "acct_1234567890",
|
|
105
|
+
auth: {
|
|
106
|
+
type: "headers",
|
|
107
|
+
headers: { Authorization: `Bearer ${env.STRIPE_PLATFORM_KEY}` },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Administrators must enable MCP access in the Stripe Dashboard, and Stripe
|
|
113
|
+
manages that setting **separately for sandbox and live mode**. A connector that
|
|
114
|
+
boots but cannot list tools is usually a dashboard toggle, not a bad key.
|
|
115
|
+
|
|
116
|
+
## The eleven tools, and what they are classified as
|
|
117
|
+
|
|
118
|
+
Stripe documents eleven tools on the hosted server. Seven are reads:
|
|
119
|
+
|
|
120
|
+
`stripe_api_search`, `stripe_api_details`, `stripe_api_read`,
|
|
121
|
+
`get_stripe_account_info`, `get_balance_summary`,
|
|
122
|
+
`search_stripe_documentation`, `stripe_implementation_planner`.
|
|
123
|
+
|
|
124
|
+
Four are writes:
|
|
125
|
+
|
|
126
|
+
`stripe_api_write` and `create_refund` are classified destructive;
|
|
127
|
+
`stripe_report` and `send_stripe_mcp_feedback` are additive.
|
|
128
|
+
|
|
129
|
+
Two of those deserve a sentence. `stripe_api_read` is a read because Stripe
|
|
130
|
+
documents it as the `GET` half of a generic pair — the tool is the read
|
|
131
|
+
boundary, not whichever endpoint an agent names inside it, and its sibling
|
|
132
|
+
`stripe_api_write` carries every `POST`, `PATCH`, `PUT`, and `DELETE`.
|
|
133
|
+
`create_refund` is filed destructive despite its name: it reverses a settled
|
|
134
|
+
charge and moves money back out, which is a mutation of something that already
|
|
135
|
+
exists rather than a fresh object appearing beside it. Additive writes
|
|
136
|
+
(`stripe_report`, `send_stripe_mcp_feedback`) leave `destructiveHint` unset;
|
|
137
|
+
`readOnlyHint: false` already routes them through `call_destructive_tool`, and
|
|
138
|
+
asserting destruction only inflates the approval copy the host shows a human.
|
|
139
|
+
|
|
140
|
+
That classification is **fill-in only**, and unconditionally so: it supplies
|
|
141
|
+
the annotations Stripe leaves unset — Stripe documents no MCP annotations at
|
|
142
|
+
all — and contradicts an explicit downstream annotation in neither direction. A
|
|
143
|
+
tool on the read allowlist arriving with `destructiveHint: true` or
|
|
144
|
+
`readOnlyHint: false` keeps exactly what the downstream said and stays behind
|
|
145
|
+
`call_destructive_tool`. A tool on neither maintained list arriving with
|
|
146
|
+
`readOnlyHint: true` keeps that too, and stays callable from `execute_code`.
|
|
147
|
+
Both are the downstream telling you this release's allowlist is stale, and on a
|
|
148
|
+
name no release has reviewed its word is the only evidence there is. The one
|
|
149
|
+
classification that still outranks the downstream is a name this release
|
|
150
|
+
reviewed and filed destructive: a `create_refund` claiming `readOnlyHint: true`
|
|
151
|
+
is a downstream bug rather than news, and stays on the approval path.
|
|
152
|
+
|
|
153
|
+
An unfamiliar tool that annotates nothing fails closed onto
|
|
154
|
+
`call_destructive_tool` until a Connecta release reviews it. That is not
|
|
155
|
+
hypothetical here: Stripe's own MCP page still carries a `create_customer`
|
|
156
|
+
example that its tool table no longer lists. Whatever the server actually
|
|
157
|
+
serves, an unclassified and unannotated `create_customer` lands on the approval
|
|
158
|
+
path. Expect the undocumented Treasury tools Stripe alludes to to arrive
|
|
159
|
+
unclassified as well — annotated ones will be taken at their word.
|
|
160
|
+
|
|
161
|
+
Stripe publishes no stability or deprecation policy for this tool set and
|
|
162
|
+
invites tool requests by email, so treat the list as unversioned. `get_balance_summary`
|
|
163
|
+
is Treasury, which Stripe labels public preview and gates behind an access
|
|
164
|
+
request — expect it to be absent unless the account is allowlisted, and expect
|
|
165
|
+
the other Treasury tools Stripe alludes to but does not document to arrive
|
|
166
|
+
unclassified.
|
|
167
|
+
|
|
168
|
+
## Rate limits
|
|
169
|
+
|
|
170
|
+
Stripe documents no rate limit specific to the MCP server. The connection
|
|
171
|
+
therefore transcribes the account limit that MCP traffic spends
|
|
172
|
+
([rate limits](https://docs.stripe.com/rate-limits)): **100 requests per second
|
|
173
|
+
in live mode, 25 in a sandbox**, and any single endpoint is capped at 25 per
|
|
174
|
+
second regardless of mode, so paging one list is the real constraint. The
|
|
175
|
+
`maxConcurrency` beside it — 8 for
|
|
176
|
+
production, 4 for sandbox — is Connecta's own conservative choice: Stripe
|
|
177
|
+
documents that per-account and per-endpoint concurrency limits exist, and
|
|
178
|
+
surface as `429` with a `Stripe-Rate-Limited-Reason` of `global-concurrency` or
|
|
179
|
+
`endpoint-concurrency`, but publishes no number.
|
|
180
|
+
|
|
181
|
+
As with every connector policy this is a **best-effort approximation** of the
|
|
182
|
+
provider's limit, not an enforcement of it. Each runtime keeps its own counter,
|
|
183
|
+
so N Worker isolates or Node processes serving one deployment can each admit up
|
|
184
|
+
to the stated rate, and the same Stripe account may be spending its budget on
|
|
185
|
+
traffic Connecta never sees. Discovery traffic is outside connector call
|
|
186
|
+
admission and still needs restrained use.
|
|
187
|
+
|
|
188
|
+
## What is not verified
|
|
189
|
+
|
|
190
|
+
Stripe's MCP documentation is silent on two things this connection had to reason
|
|
191
|
+
about rather than read:
|
|
192
|
+
|
|
193
|
+
- **How an OAuth session resolves to live versus sandbox at call time.** Stripe
|
|
194
|
+
says sessions are "scoped to … the current environment (live mode or a
|
|
195
|
+
sandbox)" and that dashboard access is managed separately per environment, but
|
|
196
|
+
never states the mechanism. The key-prefix check covers `headers` auth only;
|
|
197
|
+
for OAuth, `mode` is a declaration Connecta surfaces and cannot verify.
|
|
198
|
+
- **Whether pagination cursors and `Idempotency-Key` are passable through
|
|
199
|
+
`stripe_api_read` / `stripe_api_write`.** The conventions in the usage guide
|
|
200
|
+
are Stripe's documented API conventions; how they thread through the generic
|
|
201
|
+
tools' arguments is not documented. The guide states them because an agent
|
|
202
|
+
that ignores them is wrong either way.
|
package/ethos.md
CHANGED
|
@@ -15,9 +15,12 @@ order, and amending it is a design decision, not a drive-by edit.
|
|
|
15
15
|
- **A deployment is a small config-as-code file.** Changing what agents can
|
|
16
16
|
reach is an edit and a redeploy. One deployment, one tenant, one audience —
|
|
17
17
|
more audiences means more deployments.
|
|
18
|
-
- **
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
- **Curated when available, open when not.** Prefer an explicitly imported
|
|
19
|
+
prebuilt connection when Connecta maintains one: it carries the provider's
|
|
20
|
+
known-good endpoint, authentication defaults, tool ergonomics, and concise
|
|
21
|
+
usage guidance. `remoteMcp()` and `api()` remain equal, first-class
|
|
22
|
+
primitives for custom and unsupported integrations. Every path produces the
|
|
23
|
+
same `Connector`: same addresses, same catalog, same safety rules.
|
|
21
24
|
- **Seven tools, an executor required.** The primary surface is a program, so
|
|
22
25
|
every deployment runs an executor — a Dynamic Worker on Cloudflare, QuickJS
|
|
23
26
|
behind its optional-peer subpath on Node — and one without refuses to boot
|
|
@@ -66,6 +69,8 @@ proposing one without a new argument is not.
|
|
|
66
69
|
| Multi-tenancy / account model | refused | one deployment per tenant; deploy again instead |
|
|
67
70
|
| Policy engine, approvals, pauses | refused | the host asks the human; connecta only annotates |
|
|
68
71
|
| Runtime connector registration | refused | config-as-code is the security model |
|
|
72
|
+
| Prebuilt connections as the preferred authoring path | accepted | an a-la-carte provider constructor, imported and constructed in the deployment file, encodes maintained defaults for providers connecta actually uses — preferred *when maintained*, with no promise of one per provider; it returns exactly one ordinary `Connector` with no extra privileges — never a bundle, a group, a preset, or a registry — its tools are hand-written or proxied from a downstream MCP catalog, never generated from a schema document; its vetted annotations classify what the downstream leaves unannotated and never overrule an explicit one; `remoteMcp()` and `api()` stay first-class ([#297](https://github.com/zackbart/connecta/issues/297)) |
|
|
73
|
+
| Provider registry / integration marketplace | refused | prebuilt connections are imports, not listings; discovery happens in documentation, never at runtime ([#297](https://github.com/zackbart/connecta/issues/297)) |
|
|
69
74
|
| Protocol sessions & server push | refused | stateless per request |
|
|
70
75
|
| Resources & prompts aggregation | refused | tools only; connecta's own Apps shell is the one `resources/read` carve-out ([#266](https://github.com/zackbart/connecta/issues/266)) |
|
|
71
76
|
| Elicitation passthrough | refused | no route through a stateless aggregator |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zackbart/connecta",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of seven meta-tools.",
|
|
@@ -64,6 +64,26 @@
|
|
|
64
64
|
"./auth/clerk": {
|
|
65
65
|
"types": "./dist/auth/clerk.d.ts",
|
|
66
66
|
"import": "./dist/auth/clerk.js"
|
|
67
|
+
},
|
|
68
|
+
"./providers/cloudflare": {
|
|
69
|
+
"types": "./dist/providers/cloudflare.d.ts",
|
|
70
|
+
"import": "./dist/providers/cloudflare.js"
|
|
71
|
+
},
|
|
72
|
+
"./providers/linear": {
|
|
73
|
+
"types": "./dist/providers/linear.d.ts",
|
|
74
|
+
"import": "./dist/providers/linear.js"
|
|
75
|
+
},
|
|
76
|
+
"./providers/mixpanel": {
|
|
77
|
+
"types": "./dist/providers/mixpanel.d.ts",
|
|
78
|
+
"import": "./dist/providers/mixpanel.js"
|
|
79
|
+
},
|
|
80
|
+
"./providers/notion": {
|
|
81
|
+
"types": "./dist/providers/notion.d.ts",
|
|
82
|
+
"import": "./dist/providers/notion.js"
|
|
83
|
+
},
|
|
84
|
+
"./providers/stripe": {
|
|
85
|
+
"types": "./dist/providers/stripe.d.ts",
|
|
86
|
+
"import": "./dist/providers/stripe.js"
|
|
67
87
|
}
|
|
68
88
|
},
|
|
69
89
|
"scripts": {
|
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,19 +826,58 @@ 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;
|
|
837
|
+
// A scope that resolved to nothing is the same silence one step earlier in
|
|
838
|
+
// the lookup: no connector resolved, so no catalog was even attempted, so
|
|
839
|
+
// no catalog failed and the unavailable path below never fires. Echo only
|
|
840
|
+
// the ID the caller already supplied — naming what else is configured
|
|
841
|
+
// would answer a question they did not ask, past a filter they may not
|
|
842
|
+
// pass.
|
|
843
|
+
const unknownConnectorGuidance =
|
|
844
|
+
args.connector && !scopedConnector
|
|
845
|
+
? `Connector "${args.connector}" is not configured in this deployment. Omit connector to search all configured tools.`
|
|
846
|
+
: undefined;
|
|
847
|
+
// Term-bearing searches report analysis only when the scorer had to
|
|
848
|
+
// degrade; a browse has no terms to analyse and normally reports none at
|
|
849
|
+
// all. But neither "this catalog is unavailable" nor "there is no such
|
|
850
|
+
// connector" is a statement about terms, and answering either browse with
|
|
851
|
+
// an empty entry list alone is indistinguishable from a connector that
|
|
852
|
+
// simply exposes no tools. The term partitions stay empty on those paths
|
|
853
|
+
// because there were no terms — the scope fields carry the whole message.
|
|
854
|
+
const reportsQueryAnalysis =
|
|
855
|
+
queryTerms.length > 0
|
|
856
|
+
? matchMode === "partial"
|
|
857
|
+
: unknownConnectorGuidance !== undefined || unavailableCatalogs > 0;
|
|
743
858
|
const guidance =
|
|
744
859
|
queryTerms.length === 0
|
|
745
|
-
?
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
860
|
+
? // A browse has no terms to advise about, so it stays silent unless
|
|
861
|
+
// the scope itself failed: the guidance on a scoped miss recommends
|
|
862
|
+
// browsing with an empty query, and that advice must not lead into a
|
|
863
|
+
// dead end that looks like a connector with no tools.
|
|
864
|
+
(unknownConnectorGuidance ??
|
|
865
|
+
(unavailableCatalogs === 0
|
|
866
|
+
? undefined
|
|
749
867
|
: scopedConnector
|
|
868
|
+
? `Connector "${scopedConnector.id}" could not be browsed because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
|
|
869
|
+
: `${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable, so this browse is incomplete. Scope by connector to see the typed reason.`))
|
|
870
|
+
: matches.length === 0
|
|
871
|
+
? (unknownConnectorGuidance ??
|
|
872
|
+
(scopedConnector
|
|
750
873
|
? unavailableCatalogs > 0
|
|
751
|
-
? `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable.
|
|
752
|
-
:
|
|
874
|
+
? `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
|
|
875
|
+
: scopedGuide?.required
|
|
876
|
+
? `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}`
|
|
877
|
+
: `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Refine terms or browse it with an empty query.${filterRecovery}`
|
|
753
878
|
: unavailableCatalogs === 0
|
|
754
879
|
? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
|
|
755
|
-
: `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}`
|
|
880
|
+
: `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}`))
|
|
756
881
|
: matchMode === "partial"
|
|
757
882
|
? scopedConnector
|
|
758
883
|
? `No single tool on connector "${scopedConnector.id}" matched every term. Split distinct intents into separate searches.`
|
|
@@ -770,7 +895,7 @@ export class CatalogService {
|
|
|
770
895
|
...(matchMode === "partial" && matches.length > 0
|
|
771
896
|
? { matchMode }
|
|
772
897
|
: {}),
|
|
773
|
-
...(
|
|
898
|
+
...(reportsQueryAnalysis
|
|
774
899
|
? {
|
|
775
900
|
queryAnalysis: {
|
|
776
901
|
representedTerms,
|
|
@@ -789,6 +914,23 @@ export class CatalogService {
|
|
|
789
914
|
...(unavailableCatalogs > 0
|
|
790
915
|
? { unavailableConnectorCount: unavailableCatalogs }
|
|
791
916
|
: {}),
|
|
917
|
+
...(scopedCatalogError ? { catalogError: scopedCatalogError } : {}),
|
|
918
|
+
...(scopedGuide
|
|
919
|
+
? {
|
|
920
|
+
guide: scopedGuide.guide,
|
|
921
|
+
...(scopedGuide.guideSummary
|
|
922
|
+
? { guideSummary: scopedGuide.guideSummary }
|
|
923
|
+
: {}),
|
|
924
|
+
...(scopedGuide.required
|
|
925
|
+
? {
|
|
926
|
+
guideRequired: true as const,
|
|
927
|
+
guideRequiredReasons: [
|
|
928
|
+
"connector_required" as const,
|
|
929
|
+
],
|
|
930
|
+
}
|
|
931
|
+
: {}),
|
|
932
|
+
}
|
|
933
|
+
: {}),
|
|
792
934
|
...(guidance ? { guidance } : {}),
|
|
793
935
|
},
|
|
794
936
|
}
|
|
@@ -853,12 +995,27 @@ export class CatalogService {
|
|
|
853
995
|
tool.description,
|
|
854
996
|
args.fullDescriptions === true,
|
|
855
997
|
);
|
|
998
|
+
const requiredReasons = guideRequiredReasons(
|
|
999
|
+
addressResolution.connector,
|
|
1000
|
+
tool,
|
|
1001
|
+
false,
|
|
1002
|
+
);
|
|
1003
|
+
const guideSummary = connectorGuideSummary(addressResolution.connector);
|
|
856
1004
|
return {
|
|
857
1005
|
address,
|
|
858
1006
|
name: tool.name,
|
|
859
1007
|
...(description !== undefined ? { description } : {}),
|
|
860
1008
|
...(connectorGuide(addressResolution.connector)
|
|
861
|
-
? {
|
|
1009
|
+
? {
|
|
1010
|
+
guide: connectorSkillName(addressResolution.connector.id),
|
|
1011
|
+
...(guideSummary ? { guideSummary } : {}),
|
|
1012
|
+
}
|
|
1013
|
+
: {}),
|
|
1014
|
+
...(requiredReasons
|
|
1015
|
+
? {
|
|
1016
|
+
guideRequired: true as const,
|
|
1017
|
+
guideRequiredReasons: requiredReasons,
|
|
1018
|
+
}
|
|
862
1019
|
: {}),
|
|
863
1020
|
inputSchema: renderSchema(input, format),
|
|
864
1021
|
...(tool.outputSchema
|
|
@@ -877,6 +1034,7 @@ export function groupedSearchResult(page: CatalogSearchPage) {
|
|
|
877
1034
|
id: string;
|
|
878
1035
|
title?: string;
|
|
879
1036
|
guide?: string;
|
|
1037
|
+
guideSummary?: string;
|
|
880
1038
|
tools: CatalogSearchEntry["tool"][];
|
|
881
1039
|
}> = [];
|
|
882
1040
|
const byConnector = new Map<string, (typeof groups)[number]>();
|
|
@@ -889,6 +1047,9 @@ export function groupedSearchResult(page: CatalogSearchPage) {
|
|
|
889
1047
|
id: entry.connector.id,
|
|
890
1048
|
...(entry.connector.title ? { title: entry.connector.title } : {}),
|
|
891
1049
|
...(entry.guide ? { guide: entry.guide } : {}),
|
|
1050
|
+
...(entry.guideSummary
|
|
1051
|
+
? { guideSummary: entry.guideSummary }
|
|
1052
|
+
: {}),
|
|
892
1053
|
tools: [],
|
|
893
1054
|
};
|
|
894
1055
|
byConnector.set(entry.connector.id, group);
|
|
@@ -913,6 +1074,9 @@ export function flatSearchResult(page: CatalogSearchPage) {
|
|
|
913
1074
|
tools: page.entries.map((entry) => ({
|
|
914
1075
|
...entry.tool,
|
|
915
1076
|
...(entry.guide ? { guide: entry.guide } : {}),
|
|
1077
|
+
...(entry.guideSummary
|
|
1078
|
+
? { guideSummary: entry.guideSummary }
|
|
1079
|
+
: {}),
|
|
916
1080
|
})),
|
|
917
1081
|
total: page.total,
|
|
918
1082
|
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. */
|
|
@@ -147,7 +149,11 @@ export function api(id: string, opts: ApiOptions): Connector {
|
|
|
147
149
|
});
|
|
148
150
|
if (invalid) throw invalid;
|
|
149
151
|
}
|
|
150
|
-
return
|
|
152
|
+
// `await` (not a bare promise return) so a handler that throws before
|
|
153
|
+
// its first await never sits handler-less for the thenable-adoption
|
|
154
|
+
// microtask — workerd and vitest both report that gap as an unhandled
|
|
155
|
+
// rejection even though the caller catches the failure.
|
|
156
|
+
return await tool.handler(input, ctx);
|
|
151
157
|
},
|
|
152
158
|
};
|
|
153
159
|
}
|
|
@@ -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
|