@usethrottle/mcp 0.1.1 → 0.2.1

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/dist/lib.d.cts ADDED
@@ -0,0 +1,219 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { ActionDefinition } from '@core/action-catalog';
3
+
4
+ interface AuthCtx {
5
+ apiKey: string;
6
+ apiBaseUrl: string;
7
+ }
8
+ interface RequestOptions {
9
+ body?: unknown;
10
+ query?: Record<string, unknown>;
11
+ /**
12
+ * Sent as `Idempotency-Key`. Writes must pass one: an agent that retries is the
13
+ * normal case, and the API dedupes on
14
+ * `(workspace, application, environment, key, method, path)`.
15
+ */
16
+ idempotencyKey?: string;
17
+ /**
18
+ * Sent as `X-Throttle-Application-Id` on this one request only — see
19
+ * `PerCallOptions`.
20
+ */
21
+ applicationId?: string;
22
+ }
23
+ interface PerCallOptions {
24
+ /**
25
+ * Overrides which application a single `get`/`list` call reads from (the
26
+ * `oauth_token` auth branch validates this against the grant's own
27
+ * workspace membership; see `resolveOAuthApplication`). Scoped to this one
28
+ * request — never stored on the client instance — so concurrent calls with
29
+ * different application ids cannot bleed into each other.
30
+ */
31
+ applicationId?: string;
32
+ }
33
+ interface Pagination {
34
+ cursor: string | null;
35
+ hasMore: boolean;
36
+ }
37
+ interface ListResult<T> {
38
+ data: T[];
39
+ pagination: Pagination | null;
40
+ }
41
+ type Fetcher = typeof fetch;
42
+ declare class ThrottleClient {
43
+ private auth;
44
+ private readonly fetchImpl;
45
+ constructor(auth: AuthCtx, fetchImpl?: Fetcher);
46
+ /**
47
+ * Swap the credential every later request is sent with. The hosted remote
48
+ * server builds one client per MCP session at `initialize`, but an OAuth
49
+ * access token lives one hour and the MCP client refreshes it underneath
50
+ * the session — each request then arrives with a newer bearer than the one
51
+ * the session was opened with. Without this, the session keeps calling the
52
+ * REST API with the token it was born with and every tool fails with a 401
53
+ * the moment that first token expires, even though the caller is still
54
+ * perfectly authenticated.
55
+ */
56
+ setApiKey(apiKey: string): void;
57
+ private requestJson;
58
+ /** Unwraps the `{ data }` envelope. Key presence, not `??` — `{data: null}` unwraps to null. */
59
+ get<T>(path: string, query?: Record<string, unknown>, options?: PerCallOptions): Promise<T>;
60
+ list<T>(path: string, query?: Record<string, unknown>, options?: PerCallOptions): Promise<ListResult<T>>;
61
+ post<T>(path: string, options?: RequestOptions): Promise<T>;
62
+ patch<T>(path: string, options?: RequestOptions): Promise<T>;
63
+ }
64
+
65
+ /**
66
+ * `describe_endpoint` and `validate_payload`, answered from the OpenAPI contract
67
+ * bundled at build time. No network, no API quota.
68
+ *
69
+ * Why this exists: Throttle's public API is strict camelCase and rejects the
70
+ * retired snake_case aliases — and those aliases are exactly what a model writing
71
+ * commerce code will guess, because that is what the rest of the internet's
72
+ * examples look like. Naming the replacement turns a 400-debug loop into one call.
73
+ */
74
+ interface ContractField {
75
+ name: string;
76
+ type?: string;
77
+ required: boolean;
78
+ description?: string;
79
+ enum?: string[];
80
+ }
81
+ interface ContractOperation {
82
+ method: string;
83
+ /** OpenAPI-style path, e.g. `/api/v1/customers/{customerId}/addresses/{id}`. */
84
+ path: string;
85
+ summary?: string;
86
+ description?: string;
87
+ pathParams: string[];
88
+ queryParams: ContractField[];
89
+ bodyFields: ContractField[] | null;
90
+ }
91
+ interface ContractIndex {
92
+ generatedAt: string;
93
+ operations: ContractOperation[];
94
+ }
95
+
96
+ /**
97
+ * `search_docs`, answered from an index of the docs pages in `apps/web`,
98
+ * bundled at build time.
99
+ *
100
+ * Scoring mirrors the CLI's proven approach: title hits beat heading hits, which
101
+ * beat body hits, and an exact phrase beats scattered terms. Deliberately simple —
102
+ * the agent gets a title, a canonical docs.usethrottle.dev URL and an excerpt, then
103
+ * fetches the page itself if its client can.
104
+ */
105
+ interface DocsEntry {
106
+ title: string;
107
+ route: string;
108
+ headings: string[];
109
+ text: string;
110
+ }
111
+ interface DocsIndex {
112
+ generatedAt: string;
113
+ entries: DocsEntry[];
114
+ }
115
+
116
+ interface Identity {
117
+ type: string;
118
+ scopes: string[];
119
+ workspace: {
120
+ id: string;
121
+ name: string | null;
122
+ slug: string | null;
123
+ } | null;
124
+ application: {
125
+ id: string;
126
+ name: string | null;
127
+ slug: string | null;
128
+ } | null;
129
+ environment: {
130
+ id: string;
131
+ slug: string;
132
+ kind: string;
133
+ providerEnvironment: 'sandbox' | 'production';
134
+ };
135
+ apiKey: {
136
+ id: string;
137
+ name: string;
138
+ keyPrefix: string;
139
+ } | null;
140
+ }
141
+ interface HandlerContext {
142
+ client: ThrottleClient;
143
+ identity: Identity;
144
+ contract: ContractIndex;
145
+ docs: DocsIndex;
146
+ }
147
+
148
+ declare const VERSION: string;
149
+ interface RegistrationPolicy {
150
+ identity: Identity;
151
+ allowWrites: boolean;
152
+ allowLiveWrites: boolean;
153
+ /**
154
+ * Whether sandbox-only writes (`seed_test_data`) register without
155
+ * `allowWrites`. Defaults to true — the documented CLI behaviour, where
156
+ * seeding a non-production environment needs no flag. The hosted remote
157
+ * server passes false: it is published as strictly read-only, and a
158
+ * directory reviewer connecting with a non-production credential must not
159
+ * find a tool that creates customers and orders.
160
+ */
161
+ allowSandboxWrites?: boolean;
162
+ /**
163
+ * Whether `credentialScoped` actions (`api_get`) register. Defaults to true
164
+ * for the CLI. The hosted server passes false: on an OAuth surface every
165
+ * tool declares the scopes it asks consent for, and a tool that reads any
166
+ * path cannot.
167
+ */
168
+ allowCredentialScoped?: boolean;
169
+ }
170
+ /**
171
+ * Which tools this session gets. Pure and exported so the policy is testable
172
+ * without standing up a transport.
173
+ *
174
+ * Four independent gates, in order of how expensive it is to get them wrong:
175
+ *
176
+ * 1. **Implemented.** `0.1.0` ships the read/sandbox half; the catalog describes
177
+ * the money writes before this package implements them.
178
+ * 2. **Scope.** Delegated to the catalog's `canRunAction`, which uses the API's own
179
+ * `hasScope`. A tool the credential cannot execute is not registered at all,
180
+ * rather than failing at call time — the model never sees an affordance it
181
+ * cannot use.
182
+ * 3. **Intent.** Holding a write scope means the credential *may* write; it does
183
+ * not mean the operator intended this session to. That is `--allow-writes`.
184
+ * 4. **Blast radius.** Against a production credential, writes need
185
+ * `--allow-live-writes` on top. Sandbox-only actions invert the rule: they are
186
+ * free in a non-production environment and forbidden in production regardless
187
+ * of flags.
188
+ */
189
+ declare function registerableActions(policy: RegistrationPolicy): ActionDefinition[];
190
+ /**
191
+ * The server's `instructions`, which most clients put in front of the model before
192
+ * it picks a tool. Leads with the environment because the single most damaging
193
+ * mistake an agent can make here is not knowing whether it is looking at live data.
194
+ */
195
+ declare function buildInstructions(identity: Identity, actions: ActionDefinition[], policy: {
196
+ allowWrites: boolean;
197
+ allowLiveWrites: boolean;
198
+ }): string;
199
+ interface CreateServerOptions extends RegistrationPolicy {
200
+ client: ThrottleClient;
201
+ contract: ContractIndex;
202
+ docs: DocsIndex;
203
+ /**
204
+ * The `WWW-Authenticate` challenge value the hosting transport advertises
205
+ * (RFC 9728 `Bearer resource_metadata="..."`). When set, a tool call that
206
+ * fails because the REST API rejected the credential (HTTP 401 — an expired
207
+ * or revoked OAuth access token) carries it back as
208
+ * `_meta["mcp/www_authenticate"]`, which is what ChatGPT and other remote
209
+ * clients key their re-authorization UI on. The stdio CLI leaves it unset:
210
+ * its operator-provided key has no re-auth flow to trigger.
211
+ */
212
+ authChallenge?: string;
213
+ }
214
+ declare function createServer(options: CreateServerOptions): McpServer;
215
+
216
+ declare function loadDocsIndex(): DocsIndex;
217
+ declare function loadContractIndex(): ContractIndex;
218
+
219
+ export { type ContractIndex, type CreateServerOptions, type DocsIndex, type HandlerContext, type Identity, type RegistrationPolicy, ThrottleClient, VERSION, buildInstructions, createServer, loadContractIndex, loadDocsIndex, registerableActions };
package/dist/lib.d.ts ADDED
@@ -0,0 +1,219 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { ActionDefinition } from '@core/action-catalog';
3
+
4
+ interface AuthCtx {
5
+ apiKey: string;
6
+ apiBaseUrl: string;
7
+ }
8
+ interface RequestOptions {
9
+ body?: unknown;
10
+ query?: Record<string, unknown>;
11
+ /**
12
+ * Sent as `Idempotency-Key`. Writes must pass one: an agent that retries is the
13
+ * normal case, and the API dedupes on
14
+ * `(workspace, application, environment, key, method, path)`.
15
+ */
16
+ idempotencyKey?: string;
17
+ /**
18
+ * Sent as `X-Throttle-Application-Id` on this one request only — see
19
+ * `PerCallOptions`.
20
+ */
21
+ applicationId?: string;
22
+ }
23
+ interface PerCallOptions {
24
+ /**
25
+ * Overrides which application a single `get`/`list` call reads from (the
26
+ * `oauth_token` auth branch validates this against the grant's own
27
+ * workspace membership; see `resolveOAuthApplication`). Scoped to this one
28
+ * request — never stored on the client instance — so concurrent calls with
29
+ * different application ids cannot bleed into each other.
30
+ */
31
+ applicationId?: string;
32
+ }
33
+ interface Pagination {
34
+ cursor: string | null;
35
+ hasMore: boolean;
36
+ }
37
+ interface ListResult<T> {
38
+ data: T[];
39
+ pagination: Pagination | null;
40
+ }
41
+ type Fetcher = typeof fetch;
42
+ declare class ThrottleClient {
43
+ private auth;
44
+ private readonly fetchImpl;
45
+ constructor(auth: AuthCtx, fetchImpl?: Fetcher);
46
+ /**
47
+ * Swap the credential every later request is sent with. The hosted remote
48
+ * server builds one client per MCP session at `initialize`, but an OAuth
49
+ * access token lives one hour and the MCP client refreshes it underneath
50
+ * the session — each request then arrives with a newer bearer than the one
51
+ * the session was opened with. Without this, the session keeps calling the
52
+ * REST API with the token it was born with and every tool fails with a 401
53
+ * the moment that first token expires, even though the caller is still
54
+ * perfectly authenticated.
55
+ */
56
+ setApiKey(apiKey: string): void;
57
+ private requestJson;
58
+ /** Unwraps the `{ data }` envelope. Key presence, not `??` — `{data: null}` unwraps to null. */
59
+ get<T>(path: string, query?: Record<string, unknown>, options?: PerCallOptions): Promise<T>;
60
+ list<T>(path: string, query?: Record<string, unknown>, options?: PerCallOptions): Promise<ListResult<T>>;
61
+ post<T>(path: string, options?: RequestOptions): Promise<T>;
62
+ patch<T>(path: string, options?: RequestOptions): Promise<T>;
63
+ }
64
+
65
+ /**
66
+ * `describe_endpoint` and `validate_payload`, answered from the OpenAPI contract
67
+ * bundled at build time. No network, no API quota.
68
+ *
69
+ * Why this exists: Throttle's public API is strict camelCase and rejects the
70
+ * retired snake_case aliases — and those aliases are exactly what a model writing
71
+ * commerce code will guess, because that is what the rest of the internet's
72
+ * examples look like. Naming the replacement turns a 400-debug loop into one call.
73
+ */
74
+ interface ContractField {
75
+ name: string;
76
+ type?: string;
77
+ required: boolean;
78
+ description?: string;
79
+ enum?: string[];
80
+ }
81
+ interface ContractOperation {
82
+ method: string;
83
+ /** OpenAPI-style path, e.g. `/api/v1/customers/{customerId}/addresses/{id}`. */
84
+ path: string;
85
+ summary?: string;
86
+ description?: string;
87
+ pathParams: string[];
88
+ queryParams: ContractField[];
89
+ bodyFields: ContractField[] | null;
90
+ }
91
+ interface ContractIndex {
92
+ generatedAt: string;
93
+ operations: ContractOperation[];
94
+ }
95
+
96
+ /**
97
+ * `search_docs`, answered from an index of the docs pages in `apps/web`,
98
+ * bundled at build time.
99
+ *
100
+ * Scoring mirrors the CLI's proven approach: title hits beat heading hits, which
101
+ * beat body hits, and an exact phrase beats scattered terms. Deliberately simple —
102
+ * the agent gets a title, a canonical docs.usethrottle.dev URL and an excerpt, then
103
+ * fetches the page itself if its client can.
104
+ */
105
+ interface DocsEntry {
106
+ title: string;
107
+ route: string;
108
+ headings: string[];
109
+ text: string;
110
+ }
111
+ interface DocsIndex {
112
+ generatedAt: string;
113
+ entries: DocsEntry[];
114
+ }
115
+
116
+ interface Identity {
117
+ type: string;
118
+ scopes: string[];
119
+ workspace: {
120
+ id: string;
121
+ name: string | null;
122
+ slug: string | null;
123
+ } | null;
124
+ application: {
125
+ id: string;
126
+ name: string | null;
127
+ slug: string | null;
128
+ } | null;
129
+ environment: {
130
+ id: string;
131
+ slug: string;
132
+ kind: string;
133
+ providerEnvironment: 'sandbox' | 'production';
134
+ };
135
+ apiKey: {
136
+ id: string;
137
+ name: string;
138
+ keyPrefix: string;
139
+ } | null;
140
+ }
141
+ interface HandlerContext {
142
+ client: ThrottleClient;
143
+ identity: Identity;
144
+ contract: ContractIndex;
145
+ docs: DocsIndex;
146
+ }
147
+
148
+ declare const VERSION: string;
149
+ interface RegistrationPolicy {
150
+ identity: Identity;
151
+ allowWrites: boolean;
152
+ allowLiveWrites: boolean;
153
+ /**
154
+ * Whether sandbox-only writes (`seed_test_data`) register without
155
+ * `allowWrites`. Defaults to true — the documented CLI behaviour, where
156
+ * seeding a non-production environment needs no flag. The hosted remote
157
+ * server passes false: it is published as strictly read-only, and a
158
+ * directory reviewer connecting with a non-production credential must not
159
+ * find a tool that creates customers and orders.
160
+ */
161
+ allowSandboxWrites?: boolean;
162
+ /**
163
+ * Whether `credentialScoped` actions (`api_get`) register. Defaults to true
164
+ * for the CLI. The hosted server passes false: on an OAuth surface every
165
+ * tool declares the scopes it asks consent for, and a tool that reads any
166
+ * path cannot.
167
+ */
168
+ allowCredentialScoped?: boolean;
169
+ }
170
+ /**
171
+ * Which tools this session gets. Pure and exported so the policy is testable
172
+ * without standing up a transport.
173
+ *
174
+ * Four independent gates, in order of how expensive it is to get them wrong:
175
+ *
176
+ * 1. **Implemented.** `0.1.0` ships the read/sandbox half; the catalog describes
177
+ * the money writes before this package implements them.
178
+ * 2. **Scope.** Delegated to the catalog's `canRunAction`, which uses the API's own
179
+ * `hasScope`. A tool the credential cannot execute is not registered at all,
180
+ * rather than failing at call time — the model never sees an affordance it
181
+ * cannot use.
182
+ * 3. **Intent.** Holding a write scope means the credential *may* write; it does
183
+ * not mean the operator intended this session to. That is `--allow-writes`.
184
+ * 4. **Blast radius.** Against a production credential, writes need
185
+ * `--allow-live-writes` on top. Sandbox-only actions invert the rule: they are
186
+ * free in a non-production environment and forbidden in production regardless
187
+ * of flags.
188
+ */
189
+ declare function registerableActions(policy: RegistrationPolicy): ActionDefinition[];
190
+ /**
191
+ * The server's `instructions`, which most clients put in front of the model before
192
+ * it picks a tool. Leads with the environment because the single most damaging
193
+ * mistake an agent can make here is not knowing whether it is looking at live data.
194
+ */
195
+ declare function buildInstructions(identity: Identity, actions: ActionDefinition[], policy: {
196
+ allowWrites: boolean;
197
+ allowLiveWrites: boolean;
198
+ }): string;
199
+ interface CreateServerOptions extends RegistrationPolicy {
200
+ client: ThrottleClient;
201
+ contract: ContractIndex;
202
+ docs: DocsIndex;
203
+ /**
204
+ * The `WWW-Authenticate` challenge value the hosting transport advertises
205
+ * (RFC 9728 `Bearer resource_metadata="..."`). When set, a tool call that
206
+ * fails because the REST API rejected the credential (HTTP 401 — an expired
207
+ * or revoked OAuth access token) carries it back as
208
+ * `_meta["mcp/www_authenticate"]`, which is what ChatGPT and other remote
209
+ * clients key their re-authorization UI on. The stdio CLI leaves it unset:
210
+ * its operator-provided key has no re-auth flow to trigger.
211
+ */
212
+ authChallenge?: string;
213
+ }
214
+ declare function createServer(options: CreateServerOptions): McpServer;
215
+
216
+ declare function loadDocsIndex(): DocsIndex;
217
+ declare function loadContractIndex(): ContractIndex;
218
+
219
+ export { type ContractIndex, type CreateServerOptions, type DocsIndex, type HandlerContext, type Identity, type RegistrationPolicy, ThrottleClient, VERSION, buildInstructions, createServer, loadContractIndex, loadDocsIndex, registerableActions };