skillscript-runtime 0.33.0 → 0.34.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 +13 -0
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/index.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +29 -3
- package/dist/runtime.js.map +1 -1
- package/docs/adopter-playbook.md +11 -0
- package/docs/connector-contract-reference.md +38 -0
- package/docs/language-reference.md +77 -47
- package/examples/connectors/README.md +2 -2
- package/examples/connectors/RestConnector/.env.example +19 -0
- package/examples/connectors/RestConnector/README.md +109 -0
- package/examples/connectors/RestConnector/RestConnector.ts +326 -0
- package/examples/skillscripts/classify-support-ticket.skill.provenance.json +1 -1
- package/examples/skillscripts/data-store-roundtrip.skill.provenance.json +1 -1
- package/examples/skillscripts/doc-qa-with-citations.skill.provenance.json +1 -1
- package/examples/skillscripts/feedback-sentiment-scan.skill.provenance.json +1 -1
- package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
- package/examples/skillscripts/morning-brief.skill.provenance.json +1 -1
- package/examples/skillscripts/queue-length-monitor.skill.provenance.json +1 -1
- package/examples/skillscripts/service-health-watch.skill.provenance.json +1 -1
- package/examples/skillscripts/skill-store-roundtrip.skill.provenance.json +1 -1
- package/examples/skillscripts/youtrack-morning-sweep.skill.provenance.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RestConnector — a *working* McpConnector that fronts a plain REST/HTTP API.
|
|
3
|
+
*
|
|
4
|
+
* Unlike `McpConnectorTemplate` (a throws-`TODO` skeleton that surfaces the
|
|
5
|
+
* contract surface), this is a **complete, runnable** connector. Copy the
|
|
6
|
+
* directory, edit the `ENDPOINTS` table + base URL + auth, register it, and
|
|
7
|
+
* `$ <name>.<tool>` dispatches from a skill body hit your REST backend.
|
|
8
|
+
*
|
|
9
|
+
* ─── The point this example proves ──────────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* **The McpConnector contract is wire-protocol-agnostic.** "MCP" is in the type
|
|
12
|
+
* name because `$ connector.tool` is the *dispatch surface skills call* — it is
|
|
13
|
+
* NOT a requirement that the backend speak the MCP wire protocol.
|
|
14
|
+
*
|
|
15
|
+
* - `HttpMcpConnector` speaks JSON-RPC-over-HTTP because it fronts MCP servers.
|
|
16
|
+
* - This connector speaks plain REST because it fronts a REST API.
|
|
17
|
+
* - A `WebSocketMcpConnector`, a gRPC bridge, an in-process call — all the same.
|
|
18
|
+
*
|
|
19
|
+
* All satisfy the same two-method contract (`call` + `manifest`). A skill can't
|
|
20
|
+
* tell them apart, and **a single skill body can mix them**:
|
|
21
|
+
*
|
|
22
|
+
* $ gmail.send to="ops@acme.io" subject="Deploy done" # an MCP connector
|
|
23
|
+
* -> _
|
|
24
|
+
* $ tickets.create title="Deploy 4.2" severity="info" # this REST connector
|
|
25
|
+
* -> ticket
|
|
26
|
+
*
|
|
27
|
+
* Both are just `$ <name>.<tool>` ops. The registry holds a heterogeneous set;
|
|
28
|
+
* each op routes to whatever connector owns that name.
|
|
29
|
+
*
|
|
30
|
+
* ─── What you edit to adopt it ──────────────────────────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* 1. `ENDPOINTS` — one entry per tool: HTTP method + path (with `:param`
|
|
33
|
+
* placeholders) + a description + an optional `inputSchema` for lint.
|
|
34
|
+
* 2. `RestConnectorConfig` — base URL, auth header name, token source.
|
|
35
|
+
* 3. Register it (see README): programmatic `registerMcpConnector`, or the
|
|
36
|
+
* declarative `connectors.json` path via the `fromConfig` factory below.
|
|
37
|
+
*
|
|
38
|
+
* Everything else — path templating, query vs. body routing, auth headers,
|
|
39
|
+
* timeout, error surfacing, `staticTools()` lint, `describeTools()` discovery —
|
|
40
|
+
* works as written.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import type {
|
|
44
|
+
McpConnector,
|
|
45
|
+
McpDispatchCtx,
|
|
46
|
+
McpToolDescriptor,
|
|
47
|
+
McpConnectorCapabilities,
|
|
48
|
+
ManifestInfo,
|
|
49
|
+
} from "skillscript-runtime/connectors";
|
|
50
|
+
|
|
51
|
+
/** One REST endpoint, addressed by its skill-facing tool name (the `ENDPOINTS` key). */
|
|
52
|
+
interface RestEndpoint {
|
|
53
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
54
|
+
/**
|
|
55
|
+
* Path relative to `baseUrl`. `:name` segments are filled from `args` and
|
|
56
|
+
* consumed (so they don't also land in the query string / body).
|
|
57
|
+
* e.g. `/tickets/:id` + `{ id: 42 }` → `/tickets/42`.
|
|
58
|
+
*/
|
|
59
|
+
path: string;
|
|
60
|
+
/** Human-readable; surfaced through `describeTools()` for author-time discovery. */
|
|
61
|
+
description?: string;
|
|
62
|
+
/** JSON-Schema for the args. Feeds connector-aware input lint. Optional. */
|
|
63
|
+
inputSchema?: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The tool surface. **This is the one table you edit.** Each key is a tool name
|
|
68
|
+
* a skill dispatches as `$ <connector>.<key>`. Non-GET/DELETE methods send the
|
|
69
|
+
* (non-path) args as a JSON body; GET/DELETE send them as a query string.
|
|
70
|
+
*
|
|
71
|
+
* The example fronts a generic "tickets" service — replace wholesale.
|
|
72
|
+
*/
|
|
73
|
+
const ENDPOINTS: Record<string, RestEndpoint> = {
|
|
74
|
+
list_tickets: {
|
|
75
|
+
method: "GET",
|
|
76
|
+
path: "/tickets",
|
|
77
|
+
description: "List tickets. Optional args: status, assignee, limit → query string.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
status: { type: "string", enum: ["open", "closed", "all"] },
|
|
82
|
+
assignee: { type: "string" },
|
|
83
|
+
limit: { type: "integer", minimum: 1, maximum: 200 },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
get_ticket: {
|
|
88
|
+
method: "GET",
|
|
89
|
+
path: "/tickets/:id",
|
|
90
|
+
description: "Fetch one ticket by id.",
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
required: ["id"],
|
|
94
|
+
properties: { id: { type: ["string", "integer"] } },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
create_ticket: {
|
|
98
|
+
method: "POST",
|
|
99
|
+
path: "/tickets",
|
|
100
|
+
// Mutating. The contract has no `mutating` flag on the tool descriptor, so
|
|
101
|
+
// convey it in the description — the HTTP method already tells the story.
|
|
102
|
+
description: "Create a ticket (POST /tickets). Args become the JSON body.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
required: ["title"],
|
|
106
|
+
properties: {
|
|
107
|
+
title: { type: "string" },
|
|
108
|
+
body: { type: "string" },
|
|
109
|
+
severity: { type: "string", enum: ["info", "warn", "crit"] },
|
|
110
|
+
assignee: { type: "string" },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
update_ticket: {
|
|
115
|
+
method: "PATCH",
|
|
116
|
+
path: "/tickets/:id",
|
|
117
|
+
description: "Patch a ticket (PATCH /tickets/:id). `id` → path; rest → JSON body.",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
required: ["id"],
|
|
121
|
+
properties: {
|
|
122
|
+
id: { type: ["string", "integer"] },
|
|
123
|
+
status: { type: "string", enum: ["open", "closed"] },
|
|
124
|
+
assignee: { type: "string" },
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Connection + auth config. Passed to the constructor, or built by `fromConfig`. */
|
|
131
|
+
export interface RestConnectorConfig {
|
|
132
|
+
/** Base URL, e.g. `https://api.internal.acme.io/v1`. Trailing slash optional. */
|
|
133
|
+
baseUrl: string;
|
|
134
|
+
/**
|
|
135
|
+
* Bearer/API token. Prefer `authTokenEnvVar` over hardcoding — never commit a
|
|
136
|
+
* literal token. If both are set, `authToken` wins.
|
|
137
|
+
*/
|
|
138
|
+
authToken?: string;
|
|
139
|
+
/** Read the token from this env var at call time (e.g. `TICKETS_API_TOKEN`). */
|
|
140
|
+
authTokenEnvVar?: string;
|
|
141
|
+
/** Header carrying the token. Default `Authorization`. */
|
|
142
|
+
authHeader?: string;
|
|
143
|
+
/**
|
|
144
|
+
* Formats the header value from the token. Default `Bearer <token>`.
|
|
145
|
+
* For an `X-API-Key` style header, set `authHeader: "X-API-Key"` and
|
|
146
|
+
* `authScheme: "raw"`.
|
|
147
|
+
*/
|
|
148
|
+
authScheme?: "bearer" | "raw";
|
|
149
|
+
/** Extra headers sent on every request (Accept, tenant id, etc.). */
|
|
150
|
+
defaultHeaders?: Record<string, string>;
|
|
151
|
+
/** Per-request timeout. Default 30_000 ms. */
|
|
152
|
+
timeoutMs?: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export class RestConnector implements McpConnector {
|
|
156
|
+
private readonly baseUrl: string;
|
|
157
|
+
private readonly cfg: RestConnectorConfig;
|
|
158
|
+
|
|
159
|
+
constructor(config: RestConnectorConfig) {
|
|
160
|
+
if (!config?.baseUrl || typeof config.baseUrl !== "string") {
|
|
161
|
+
throw new Error("RestConnector: `baseUrl` is required.");
|
|
162
|
+
}
|
|
163
|
+
// Normalize: drop a trailing slash so `baseUrl + path` never doubles up.
|
|
164
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
165
|
+
this.cfg = config;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Declare what this impl supports. A plain REST wrapper does none of the
|
|
170
|
+
* advanced dispatch features by default — flip a flag only once you actually
|
|
171
|
+
* implement it (see the `ctxOverrides` note in `call()` for identity).
|
|
172
|
+
*/
|
|
173
|
+
static staticCapabilities(): McpConnectorCapabilities {
|
|
174
|
+
return {
|
|
175
|
+
connector_type: "mcp_connector",
|
|
176
|
+
implementation: "RestConnector",
|
|
177
|
+
contract_version: "1.0.0",
|
|
178
|
+
features: {
|
|
179
|
+
supports_identity_propagation: false,
|
|
180
|
+
supports_streaming_responses: false,
|
|
181
|
+
supports_batch: false,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Closed-set tool surface → lint validates `$ name.tool` at authoring time.
|
|
188
|
+
* Because our tools are a fixed table, we can return it: a skill that writes
|
|
189
|
+
* `$ tickets.delete_everything` fails lint with `unknown-tool-on-connector`
|
|
190
|
+
* instead of failing at runtime. (Connectors whose surface is discovered at
|
|
191
|
+
* runtime return `null` / omit this — see `McpConnectorTemplate`.)
|
|
192
|
+
*/
|
|
193
|
+
static staticTools(): string[] {
|
|
194
|
+
return Object.keys(ENDPOINTS);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Declarative-wiring factory for `connectors.json`. Register the class once in
|
|
199
|
+
* your bootstrap (see README), then adopters declare instances in JSON:
|
|
200
|
+
*
|
|
201
|
+
* { "tickets": { "class": "RestConnector",
|
|
202
|
+
* "config": { "baseUrl": "https://...", "authTokenEnvVar": "TICKETS_API_TOKEN" } } }
|
|
203
|
+
*
|
|
204
|
+
* Note this makes the *instance* JSON-configurable; the `ENDPOINTS` table is
|
|
205
|
+
* still code. A fully config-only REST connector (endpoints in JSON too) is a
|
|
206
|
+
* natural next fork — the mechanics here are the same.
|
|
207
|
+
*/
|
|
208
|
+
static fromConfig(config: Record<string, unknown>): RestConnector {
|
|
209
|
+
if (typeof config.baseUrl !== "string") {
|
|
210
|
+
throw new Error("RestConnector.fromConfig: `config.baseUrl` (string) is required.");
|
|
211
|
+
}
|
|
212
|
+
return new RestConnector(config as unknown as RestConnectorConfig);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Dispatch `$ <connector>.<toolName>` → an HTTPS request.
|
|
217
|
+
*
|
|
218
|
+
* `ctxOverrides` carries the caller identity (`agentId`, `isAdmin`). This
|
|
219
|
+
* example ignores it (`supports_identity_propagation: false`). To honor it,
|
|
220
|
+
* forward it as a header here — e.g. `headers["X-On-Behalf-Of"] = ctx.agentId`
|
|
221
|
+
* — and flip the capability flag (which then obligates the Level 1/Level 2
|
|
222
|
+
* conformance probes; see `McpConnectorTemplate` for that contract).
|
|
223
|
+
*/
|
|
224
|
+
async call(
|
|
225
|
+
toolName: string,
|
|
226
|
+
args: Record<string, unknown>,
|
|
227
|
+
_ctxOverrides?: McpDispatchCtx,
|
|
228
|
+
): Promise<unknown> {
|
|
229
|
+
const ep = ENDPOINTS[toolName];
|
|
230
|
+
if (!ep) {
|
|
231
|
+
const known = Object.keys(ENDPOINTS).join(", ");
|
|
232
|
+
throw new Error(`RestConnector: unknown tool "${toolName}". Known tools: ${known}.`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const supplied = { ...(args ?? {}) };
|
|
236
|
+
|
|
237
|
+
// Fill `:param` path segments from args; consume them so they don't also
|
|
238
|
+
// appear in the query string / body.
|
|
239
|
+
const path = ep.path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, name: string) => {
|
|
240
|
+
if (supplied[name] === undefined) {
|
|
241
|
+
throw new Error(`RestConnector: "${toolName}" needs path param "${name}".`);
|
|
242
|
+
}
|
|
243
|
+
const val = String(supplied[name]);
|
|
244
|
+
delete supplied[name];
|
|
245
|
+
return encodeURIComponent(val);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const url = new URL(this.baseUrl + path);
|
|
249
|
+
const hasBody = ep.method !== "GET" && ep.method !== "DELETE";
|
|
250
|
+
|
|
251
|
+
if (!hasBody) {
|
|
252
|
+
for (const [k, v] of Object.entries(supplied)) {
|
|
253
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const headers: Record<string, string> = {
|
|
258
|
+
Accept: "application/json",
|
|
259
|
+
...(this.cfg.defaultHeaders ?? {}),
|
|
260
|
+
};
|
|
261
|
+
if (hasBody) headers["Content-Type"] = "application/json";
|
|
262
|
+
|
|
263
|
+
const token = this.cfg.authToken ??
|
|
264
|
+
(this.cfg.authTokenEnvVar ? process.env[this.cfg.authTokenEnvVar] : undefined);
|
|
265
|
+
if (token) {
|
|
266
|
+
const headerName = this.cfg.authHeader ?? "Authorization";
|
|
267
|
+
headers[headerName] = this.cfg.authScheme === "raw" ? token : `Bearer ${token}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let res: Response;
|
|
271
|
+
try {
|
|
272
|
+
res = await fetch(url, {
|
|
273
|
+
method: ep.method,
|
|
274
|
+
headers,
|
|
275
|
+
body: hasBody ? JSON.stringify(supplied) : undefined,
|
|
276
|
+
signal: AbortSignal.timeout(this.cfg.timeoutMs ?? 30_000),
|
|
277
|
+
});
|
|
278
|
+
} catch (err) {
|
|
279
|
+
// Network / timeout — throw so the skill's op-level `(fallback: ...)`
|
|
280
|
+
// can catch it. Never return an error envelope silently.
|
|
281
|
+
throw new Error(
|
|
282
|
+
`RestConnector: ${ep.method} ${url.pathname} failed: ${(err as Error).message}`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const text = await res.text();
|
|
287
|
+
if (!res.ok) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`RestConnector: ${ep.method} ${url.pathname} → HTTP ${res.status}. ${text.slice(0, 500)}`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Return parsed JSON when the server sends it; fall back to raw text.
|
|
294
|
+
if (!text) return null;
|
|
295
|
+
try {
|
|
296
|
+
return JSON.parse(text);
|
|
297
|
+
} catch {
|
|
298
|
+
return text;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Author-time tool discovery. The contract descriptor is
|
|
304
|
+
* `{ name, description?, inputSchema? }` — there is no `mutating` field, so
|
|
305
|
+
* the mutating/read distinction rides in the description + the method prefix.
|
|
306
|
+
*/
|
|
307
|
+
async describeTools(): Promise<McpToolDescriptor[]> {
|
|
308
|
+
return Object.entries(ENDPOINTS).map(([name, ep]) => ({
|
|
309
|
+
name,
|
|
310
|
+
description: `[${ep.method}] ${ep.description ?? ""}`.trim(),
|
|
311
|
+
inputSchema: ep.inputSchema,
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Capability snapshot for `runtime_capabilities` discovery. */
|
|
316
|
+
async manifest(): Promise<ManifestInfo<"mcp_connector">> {
|
|
317
|
+
return {
|
|
318
|
+
capabilities_version: "1",
|
|
319
|
+
manifest: {
|
|
320
|
+
kind: "rest",
|
|
321
|
+
base_url: this.baseUrl,
|
|
322
|
+
tools_available: Object.keys(ENDPOINTS),
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillscript-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Runtime, compiler, lint, CLI, and dashboard for Skillscript — a small declarative language for authoring agent workflows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Scott Shwarts <scotts@pobox.com>",
|