@zapier/connectors-sdk 0.1.0-experimental.11 → 0.1.0-experimental.13

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.
@@ -48,16 +48,16 @@ import {
48
48
 
49
49
  A **surface** is how a `ToolDefinition` is exposed to a consumer. **Registration surfaces** publish tool metadata; **execution surfaces** run a tool.
50
50
 
51
- | Surface | Kind | Helper | Surface shape / behavior |
52
- | ----------------------------- | ------------ | ------------------------------ | -------------------------------------------------------------------------------------------- |
53
- | MCP `tools/list` (descriptor) | registration | `toMcpTool` | Wire-format `Tool` (JSON Schema'd `inputSchema`, optional `_meta`) |
54
- | MCP `McpServer.registerTool` | registration | `toMcpServerTool` | `registerTool` config (Zod schemas pass-through, optional `_meta`) |
55
- | OpenAI Chat Completions | registration | `toChatCompletionTool` | `{ type: "function", function: { name, description, parameters } }` |
56
- | OpenAI Responses | registration | `toResponsesTool` | `{ type: "function", name, description, parameters }` |
57
- | Per-script CLI | execution | `handleIfScriptMain` | argv/stdin JSON in → wrapped `script.run` → JSON stdout |
58
- | Connector bin | execution | `runDispatchCli` | `<bin> run <script> [args…]` → per-script CLI; `<bin> mcp` → local MCP server |
59
- | Local MCP server | execution | `runDispatchCli` (`<bin> mcp`) | Stdio MCP server registering every script as a native MCP tool (`tools/list` + `tools/call`) |
60
- | In-process run | execution | `script.run` / named export | Typed input + `RunOptions` → output |
51
+ | Surface | Kind | Helper | Surface shape / behavior |
52
+ | ----------------------------- | ------------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
+ | MCP `tools/list` (descriptor) | registration | `toMcpTool` | Wire-format `Tool` (JSON Schema'd `inputSchema`, optional `_meta`) |
54
+ | MCP `McpServer.registerTool` | registration | `toMcpServerTool` | `registerTool` config (Zod schemas pass-through, optional `_meta`) |
55
+ | OpenAI Chat Completions | registration | `toChatCompletionTool` | `{ type: "function", function: { name, description, parameters } }` |
56
+ | OpenAI Responses | registration | `toResponsesTool` | `{ type: "function", name, description, parameters }` |
57
+ | Per-script CLI | execution | `handleIfScriptMain` | argv/stdin JSON in → wrapped `script.run` → JSON stdout |
58
+ | Connector bin | execution | `runDispatchCli` | `<bin> run <script> [args…]` → per-script CLI; `<bin> mcp` → local MCP server |
59
+ | Local MCP server | execution | `runDispatchCli` (`<bin> mcp`) | Stdio MCP server: every script as a tool (`tools/list` + `tools/call`), plus `SKILL.md` + `references/*.md` as resources (`resources/list` + `resources/read`) |
60
+ | In-process run | execution | `script.run` / named export | Typed input + `RunOptions` → output |
61
61
 
62
62
  ## Authoring a script — `defineTool`
63
63
 
@@ -353,7 +353,8 @@ Reached through the bundled CLI as `npx @zapier/<x>-connector mcp` — no per-ap
353
353
  1. Builds an in-memory registry keyed by `script.name`, with each entry holding the script and `RunOptions` resolved once via `buildRunOptionsFromEnv` against the connector's resolvers.
354
354
  2. Resolves server identity (`name` / `version`) from the `package.json` adjacent to the connector's `cli.ts` (via `meta.url`).
355
355
  3. For each script, calls `server.registerTool(script.name, toMcpServerTool(script), cb)` — `McpServer` handles `tools/list` and runs the Zod input parse before dispatching to `script.run(input, runOpts)`. The callback returns the result as both `structuredContent` and a JSON-stringified text part.
356
- 4. Connects via `StdioServerTransport` and writes a one-line ready banner to stderr.
356
+ 4. For each bundled doc — `SKILL.md` and each `references/*.md` (discovered recursively) — calls `server.registerResource(...)` with a stable `connector://<slug>/…` URI and `text/markdown` type. Content is re-read from disk on each `resources/read` (link-rewriting included, so it matches the CLI's `skill` / `reference` output). Skipped entirely when `meta.url` is absent or the connector ships no docs, so the `resources` capability is only advertised when there is something to serve.
357
+ 5. Connects via `StdioServerTransport` and writes a one-line ready banner to stderr.
357
358
 
358
359
  ```bash
359
360
  NOTION_TOKEN=secret_xxx npx @zapier/notion-connector mcp
package/dist/index.cjs CHANGED
@@ -706,14 +706,74 @@ function defineTool(config) {
706
706
  var import_zod3 = require("zod");
707
707
 
708
708
  // src/surfaces/run-dispatch-cli.ts
709
- var fs2 = __toESM(require("fs/promises"), 1);
710
- var path2 = __toESM(require("path"), 1);
711
- var import_node_url2 = require("url");
709
+ var fs3 = __toESM(require("fs/promises"), 1);
710
+ var path3 = __toESM(require("path"), 1);
711
+ var import_node_url3 = require("url");
712
712
 
713
- // src/surfaces/serve-mcp-stdio.ts
713
+ // src/surfaces/connector-docs.ts
714
714
  var fs = __toESM(require("fs/promises"), 1);
715
715
  var path = __toESM(require("path"), 1);
716
716
  var import_node_url = require("url");
717
+ var SKILL_FILENAME = "SKILL.md";
718
+ var REFERENCES_DIRNAME = "references";
719
+ function connectorDirFromMeta(meta) {
720
+ const dir = path.dirname((0, import_node_url.fileURLToPath)(meta.url));
721
+ return path.basename(dir) === "dist" ? path.dirname(dir) : dir;
722
+ }
723
+ function skillPath(connectorDir) {
724
+ return path.join(connectorDir, SKILL_FILENAME);
725
+ }
726
+ function referencePath(connectorDir, name) {
727
+ return path.join(connectorDir, REFERENCES_DIRNAME, `${name}.md`);
728
+ }
729
+ async function loadSkillDoc(connectorDir) {
730
+ return loadDoc(skillPath(connectorDir), connectorDir);
731
+ }
732
+ async function listReferenceNames(connectorDir) {
733
+ try {
734
+ const entries = await fs.readdir(
735
+ path.join(connectorDir, REFERENCES_DIRNAME),
736
+ { recursive: true }
737
+ );
738
+ return entries.filter((entry) => entry.endsWith(".md")).map((entry) => toPosix(entry).slice(0, -".md".length));
739
+ } catch {
740
+ return [];
741
+ }
742
+ }
743
+ function toPosix(p) {
744
+ return p.split(path.sep).join("/");
745
+ }
746
+ async function loadReferenceDoc(connectorDir, name) {
747
+ return loadDoc(
748
+ referencePath(connectorDir, name),
749
+ path.join(connectorDir, REFERENCES_DIRNAME)
750
+ );
751
+ }
752
+ async function loadDoc(filePath, baseDir) {
753
+ let raw;
754
+ try {
755
+ raw = await fs.readFile(filePath, "utf8");
756
+ } catch {
757
+ return void 0;
758
+ }
759
+ return { path: filePath, content: resolveRelativeLinks(raw, baseDir) };
760
+ }
761
+ function resolveRelativeLinks(content, baseDir) {
762
+ return content.replace(
763
+ /(!?\[([^\]]*)\])\(([^)]+)\)/g,
764
+ (match, prefix, _alt, url) => {
765
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file:") || url.startsWith("#") || url.startsWith("/")) {
766
+ return match;
767
+ }
768
+ return `${prefix}(${path.resolve(baseDir, url)})`;
769
+ }
770
+ );
771
+ }
772
+
773
+ // src/surfaces/serve-mcp-stdio.ts
774
+ var fs2 = __toESM(require("fs/promises"), 1);
775
+ var path2 = __toESM(require("path"), 1);
776
+ var import_node_url2 = require("url");
717
777
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
718
778
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
719
779
  function buildMcpRegistry(scripts, env, connectionResolvers) {
@@ -734,9 +794,9 @@ async function resolveServerInfo(meta) {
734
794
  return { name: FALLBACK_SERVER_NAME, version: FALLBACK_SERVER_VERSION };
735
795
  }
736
796
  try {
737
- const cliPath = (0, import_node_url.fileURLToPath)(meta.url);
738
- const pkgPath = path.join(path.dirname(cliPath), "package.json");
739
- const raw = await fs.readFile(pkgPath, "utf8");
797
+ const cliPath = (0, import_node_url2.fileURLToPath)(meta.url);
798
+ const pkgPath = path2.join(path2.dirname(cliPath), "package.json");
799
+ const raw = await fs2.readFile(pkgPath, "utf8");
740
800
  const pkg = JSON.parse(raw);
741
801
  return {
742
802
  name: typeof pkg.name === "string" ? pkg.name : FALLBACK_SERVER_NAME,
@@ -746,9 +806,8 @@ async function resolveServerInfo(meta) {
746
806
  return { name: FALLBACK_SERVER_NAME, version: FALLBACK_SERVER_VERSION };
747
807
  }
748
808
  }
749
- async function serveMcpStdio(meta, connector, opts = {}) {
809
+ async function buildMcpServer(meta, connector, opts = {}) {
750
810
  const env = opts.env ?? process.env;
751
- const stderr = opts.stderr ?? process.stderr;
752
811
  const scripts = connector.scripts;
753
812
  const connectionResolvers = connector.connectionResolvers;
754
813
  const registry = buildMcpRegistry(scripts, env, connectionResolvers);
@@ -772,10 +831,69 @@ async function serveMcpStdio(meta, connector, opts = {}) {
772
831
  }
773
832
  );
774
833
  }
834
+ if (meta.url) {
835
+ await registerDocResources(
836
+ server,
837
+ connectorSlug(serverInfo.name),
838
+ connectorDirFromMeta(meta)
839
+ );
840
+ }
841
+ return { server, serverInfo };
842
+ }
843
+ async function serveMcpStdio(meta, connector, opts = {}) {
844
+ const stderr = opts.stderr ?? process.stderr;
845
+ const { server, serverInfo } = await buildMcpServer(meta, connector, opts);
775
846
  await server.connect(new import_stdio.StdioServerTransport());
776
847
  stderr.write(`[${serverInfo.name}] MCP server ready on stdio.
777
848
  `);
778
849
  }
850
+ var MARKDOWN_MIME_TYPE = "text/markdown";
851
+ function connectorSlug(serverName) {
852
+ const basename2 = serverName.split("/").pop() ?? serverName;
853
+ return basename2.replace(/-connector$/, "") || basename2;
854
+ }
855
+ async function registerDocResources(server, slug, connectorDir) {
856
+ const skill = await loadSkillDoc(connectorDir);
857
+ if (skill) {
858
+ server.registerResource(
859
+ SKILL_FILENAME,
860
+ `connector://${slug}/${SKILL_FILENAME}`,
861
+ {
862
+ title: "Skill guide",
863
+ description: "The connector's SKILL.md: when to use it, auth setup, and the script catalog.",
864
+ mimeType: MARKDOWN_MIME_TYPE
865
+ },
866
+ readDoc(() => loadSkillDoc(connectorDir), skill.path)
867
+ );
868
+ }
869
+ for (const name of await listReferenceNames(connectorDir)) {
870
+ const resourceName = `${REFERENCES_DIRNAME}/${name}.md`;
871
+ const uriPath = name.split("/").map(encodeURIComponent).join("/");
872
+ server.registerResource(
873
+ resourceName,
874
+ `connector://${slug}/${REFERENCES_DIRNAME}/${uriPath}.md`,
875
+ {
876
+ title: `Reference: ${name}`,
877
+ description: `Durable per-app knowledge from references/${name}.md (API gotchas, encodings, edge cases).`,
878
+ mimeType: MARKDOWN_MIME_TYPE
879
+ },
880
+ readDoc(() => loadReferenceDoc(connectorDir, name), resourceName)
881
+ );
882
+ }
883
+ }
884
+ function readDoc(load, label) {
885
+ return async (uri) => {
886
+ const doc = await load();
887
+ if (!doc) {
888
+ throw new Error(`Resource no longer available: ${label}.`);
889
+ }
890
+ return {
891
+ contents: [
892
+ { uri: uri.href, mimeType: MARKDOWN_MIME_TYPE, text: doc.content }
893
+ ]
894
+ };
895
+ };
896
+ }
779
897
 
780
898
  // src/surfaces/run-dispatch-cli.ts
781
899
  function asAnyDefinition(value) {
@@ -845,18 +963,14 @@ async function runDispatchCliBody(connector, opts) {
845
963
  "runDispatchCliBody: `skill` requires `meta` in options (the connector's `import.meta` from `cli.ts`)."
846
964
  );
847
965
  }
848
- const cliPath = (0, import_node_url2.fileURLToPath)(opts.meta.url);
849
- const connectorDir = path2.dirname(cliPath);
850
- const skillPath = path2.join(connectorDir, "SKILL.md");
851
- let content;
852
- try {
853
- content = await fs2.readFile(skillPath, "utf8");
854
- } catch {
966
+ const connectorDir = connectorDirFromMeta(opts.meta);
967
+ const skill = await loadSkillDoc(connectorDir);
968
+ if (!skill) {
855
969
  throw new Error(
856
- `SKILL.md not found at ${skillPath}. Ensure the connector ships a SKILL.md.`
970
+ `SKILL.md not found at ${skillPath(connectorDir)}. Ensure the connector ships a SKILL.md.`
857
971
  );
858
972
  }
859
- opts.stdout.write(resolveRelativeLinks(content, connectorDir));
973
+ opts.stdout.write(skill.content);
860
974
  return;
861
975
  }
862
976
  if (first === "reference") {
@@ -865,18 +979,10 @@ async function runDispatchCliBody(connector, opts) {
865
979
  "runDispatchCliBody: `reference` requires `meta` in options (the connector's `import.meta` from `cli.ts`)."
866
980
  );
867
981
  }
868
- const cliPath = (0, import_node_url2.fileURLToPath)(opts.meta.url);
869
- const connectorDir = path2.dirname(cliPath);
870
- const refsDir = path2.join(connectorDir, "references");
982
+ const connectorDir = connectorDirFromMeta(opts.meta);
871
983
  const name = args[1];
872
984
  if (!name) {
873
- let entries;
874
- try {
875
- const files = await fs2.readdir(refsDir);
876
- entries = files.filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -".md".length));
877
- } catch {
878
- entries = [];
879
- }
985
+ const entries = await listReferenceNames(connectorDir);
880
986
  if (entries.length === 0) {
881
987
  opts.stdout.write(`Available references: <none>
882
988
  `);
@@ -890,16 +996,13 @@ async function runDispatchCliBody(connector, opts) {
890
996
  }
891
997
  return;
892
998
  }
893
- const refPath = path2.join(refsDir, `${name}.md`);
894
- let content;
895
- try {
896
- content = await fs2.readFile(refPath, "utf8");
897
- } catch {
999
+ const reference = await loadReferenceDoc(connectorDir, name);
1000
+ if (!reference) {
898
1001
  throw new Error(
899
- `Reference file not found at ${refPath}. Ensure the connector ships \`references/${name}.md\`.`
1002
+ `Reference file not found at ${referencePath(connectorDir, name)}. Ensure the connector ships \`references/${name}.md\`.`
900
1003
  );
901
1004
  }
902
- opts.stdout.write(resolveRelativeLinks(content, refsDir));
1005
+ opts.stdout.write(reference.content);
903
1006
  return;
904
1007
  }
905
1008
  if (first !== "run") {
@@ -932,23 +1035,12 @@ async function runDispatchCliBody(connector, opts) {
932
1035
  invocation
933
1036
  });
934
1037
  }
935
- function resolveRelativeLinks(content, baseDir) {
936
- return content.replace(
937
- /(!?\[([^\]]*)\])\(([^)]+)\)/g,
938
- (match, prefix, _alt, url) => {
939
- if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file:") || url.startsWith("#") || url.startsWith("/")) {
940
- return match;
941
- }
942
- return `${prefix}(${path2.resolve(baseDir, url)})`;
943
- }
944
- );
945
- }
946
1038
  async function resolvePackageName(meta) {
947
1039
  if (!meta?.url) return void 0;
948
1040
  try {
949
- const cliPath = (0, import_node_url2.fileURLToPath)(meta.url);
950
- const pkgPath = path2.join(path2.dirname(cliPath), "package.json");
951
- const raw = await fs2.readFile(pkgPath, "utf8");
1041
+ const cliPath = (0, import_node_url3.fileURLToPath)(meta.url);
1042
+ const pkgPath = path3.join(path3.dirname(cliPath), "package.json");
1043
+ const raw = await fs3.readFile(pkgPath, "utf8");
952
1044
  const pkg = JSON.parse(raw);
953
1045
  return typeof pkg.name === "string" ? pkg.name : void 0;
954
1046
  } catch {
@@ -958,8 +1050,8 @@ async function resolvePackageName(meta) {
958
1050
  function printUsage(scripts, out, packageName) {
959
1051
  const names = Object.keys(scripts);
960
1052
  out.write(
961
- `Usage: <bin> run <script> [<json-input>]
962
- <bin> run <script> --help (per-script input + required env vars)
1053
+ `Usage: <bin> run <script> [<json-input>] [--filter <jq>]
1054
+ <bin> run <script> --help (per-script input/output schema, env vars, --filter)
963
1055
  <bin> mcp (run as a local MCP server over stdio)
964
1056
  <bin> skill (print SKILL.md to stdout)
965
1057
  <bin> reference [<name>] (print references/<name>.md, or list all)
@@ -988,7 +1080,7 @@ function printUsage(scripts, out, packageName) {
988
1080
  }
989
1081
  out.write(
990
1082
  `
991
- Credentials are environment-variable only \u2014 run \`<bin> run <script> --help\` to list each script's required env vars.
1083
+ Run \`<bin> run <script> --help\` for a script's input/output schema, its required env vars (credentials are environment-variable only), and the \`--filter <jq>\` flag for trimming output.
992
1084
  `
993
1085
  );
994
1086
  }
@@ -1014,6 +1106,13 @@ env-only \u2014 set the env vars for each script you intend to call, listed
1014
1106
  by \`<bin> run <script> --help\`. Resolution happens once per script at
1015
1107
  server start; credentials never traverse the MCP transport.
1016
1108
 
1109
+ `
1110
+ );
1111
+ out.write(
1112
+ `The connector's docs are exposed as MCP resources (text/markdown): its
1113
+ SKILL.md and each references/*.md file, mirroring \`<bin> skill\` and
1114
+ \`<bin> reference\`. Clients read them via resources/list + resources/read.
1115
+
1017
1116
  `
1018
1117
  );
1019
1118
  const names = Object.keys(scripts);
@@ -1057,12 +1156,31 @@ async function handleIfScriptMainBody(wrappedScript, connectionResolvers, io) {
1057
1156
  const args = io.argv.slice(2);
1058
1157
  let positional;
1059
1158
  let helpRequested = false;
1060
- for (const arg of args) {
1159
+ let filter;
1160
+ for (let i = 0; i < args.length; i++) {
1161
+ const arg = args[i];
1061
1162
  if (arg === "--help" || arg === "-h") {
1062
1163
  helpRequested = true;
1164
+ } else if (arg === "--filter") {
1165
+ const value = args[i + 1];
1166
+ if (value === void 0 || value.length === 0) {
1167
+ throw new Error(
1168
+ `\`--filter\` requires a jq expression, e.g. \`--filter '.results | length'\`.`
1169
+ );
1170
+ }
1171
+ filter = value;
1172
+ i++;
1173
+ } else if (arg.startsWith("--filter=")) {
1174
+ const value = arg.slice("--filter=".length);
1175
+ if (value.length === 0) {
1176
+ throw new Error(
1177
+ `\`--filter\` requires a jq expression, e.g. \`--filter '.results | length'\`.`
1178
+ );
1179
+ }
1180
+ filter = value;
1063
1181
  } else if (arg.startsWith("--")) {
1064
1182
  throw new Error(
1065
- `Unknown flag "${arg}". The per-script CLI accepts only \`--help\` and a positional JSON-encoded input. Credentials are env-only \u2014 set the resolver's required env vars (run with \`--help\` to list them).`
1183
+ `Unknown flag "${arg}". The per-script CLI accepts \`--help\`, \`--filter <jq>\`, and a positional JSON-encoded input. Credentials are env-only \u2014 set the resolver's required env vars (run with \`--help\` to list them).`
1066
1184
  );
1067
1185
  } else if (positional === void 0) {
1068
1186
  positional = arg;
@@ -1107,7 +1225,12 @@ async function handleIfScriptMainBody(wrappedScript, connectionResolvers, io) {
1107
1225
  }
1108
1226
  }
1109
1227
  const result = await wrappedScript.run(input, runOpts);
1110
- io.stdout.write(JSON.stringify(result, null, 2) + "\n");
1228
+ const output = filter === void 0 ? result : await applyJqFilter(result, filter);
1229
+ io.stdout.write(JSON.stringify(output, null, 2) + "\n");
1230
+ }
1231
+ async function applyJqFilter(value, filter) {
1232
+ const { json: runJq } = await import("jq-wasm");
1233
+ return runJq(value, filter);
1111
1234
  }
1112
1235
  async function readStdinInput(stdin, scriptName) {
1113
1236
  if (stdin.isTTY) {
@@ -1126,11 +1249,19 @@ function buildHelpText(definition, connectionResolvers, opts = {}) {
1126
1249
  lines.push("");
1127
1250
  lines.push("Usage:");
1128
1251
  if (opts.invocation) {
1129
- lines.push(` <bin> run ${opts.invocation.scriptName} '<json-input>'`);
1252
+ lines.push(
1253
+ ` <bin> run ${opts.invocation.scriptName} '<json-input>' [--filter <jq>]`
1254
+ );
1130
1255
  } else {
1131
- lines.push(` <bin> '<json-input>'`);
1256
+ lines.push(` <bin> '<json-input>' [--filter <jq>]`);
1132
1257
  }
1133
1258
  lines.push("");
1259
+ lines.push(
1260
+ " --filter <jq> Transform the JSON output through a jq expression."
1261
+ );
1262
+ lines.push(" Useful for trimming large outputs, e.g.");
1263
+ lines.push(" --filter '.results | length'.");
1264
+ lines.push("");
1134
1265
  if (opts.invocation) {
1135
1266
  lines.push("Example:");
1136
1267
  lines.push(
@@ -1155,6 +1286,11 @@ function buildHelpText(definition, connectionResolvers, opts = {}) {
1155
1286
  lines.push(...inputBlock);
1156
1287
  lines.push("");
1157
1288
  }
1289
+ const outputBlock = formatHelpForOutput(definition);
1290
+ if (outputBlock.length > 0) {
1291
+ lines.push(...outputBlock);
1292
+ lines.push("");
1293
+ }
1158
1294
  return lines.join("\n");
1159
1295
  }
1160
1296
  function formatExampleCommand(definition, connectionResolvers, invocation) {
@@ -1191,6 +1327,20 @@ function formatHelpForInput(definition) {
1191
1327
  }
1192
1328
  return lines;
1193
1329
  }
1330
+ function formatHelpForOutput(definition) {
1331
+ let schema;
1332
+ try {
1333
+ schema = import_zod3.z.toJSONSchema(definition.outputSchema);
1334
+ } catch {
1335
+ return [];
1336
+ }
1337
+ const lines = ["Output (JSON Schema):"];
1338
+ const json = JSON.stringify(schema, null, 2);
1339
+ for (const line of json.split("\n")) {
1340
+ lines.push(` ${line}`);
1341
+ }
1342
+ return lines;
1343
+ }
1194
1344
 
1195
1345
  // src/surfaces/to-functions.ts
1196
1346
  function toFunctions(connector) {
package/dist/index.d.cts CHANGED
@@ -463,8 +463,9 @@ declare function defineTool<TIn extends z.ZodObject<z.ZodRawShape>, TOut extends
463
463
  * from `process.env` via `buildRunOptionsFromEnv`, calls the wrapped
464
464
  * script, writes JSON.
465
465
  *
466
- * **Credentials are env-only by design.** The CLI takes only positional
467
- * JSON (the script's `inputSchema`) and `--help`. Passing secrets via
466
+ * **Credentials are env-only by design.** The CLI takes positional
467
+ * JSON (the script's `inputSchema`), `--help`, and `--filter <jq>` (a jq
468
+ * expression that post-processes the JSON output). Passing secrets via
468
469
  * argv would leak them through shell history, `ps`, audit logs, and CI
469
470
  * runner echo.
470
471
  *
package/dist/index.d.ts CHANGED
@@ -463,8 +463,9 @@ declare function defineTool<TIn extends z.ZodObject<z.ZodRawShape>, TOut extends
463
463
  * from `process.env` via `buildRunOptionsFromEnv`, calls the wrapped
464
464
  * script, writes JSON.
465
465
  *
466
- * **Credentials are env-only by design.** The CLI takes only positional
467
- * JSON (the script's `inputSchema`) and `--help`. Passing secrets via
466
+ * **Credentials are env-only by design.** The CLI takes positional
467
+ * JSON (the script's `inputSchema`), `--help`, and `--filter <jq>` (a jq
468
+ * expression that post-processes the JSON output). Passing secrets via
468
469
  * argv would leak them through shell history, `ps`, audit logs, and CI
469
470
  * runner echo.
470
471
  *
package/dist/index.js CHANGED
@@ -600,14 +600,74 @@ function defineTool(config) {
600
600
  import { z as z3 } from "zod";
601
601
 
602
602
  // src/surfaces/run-dispatch-cli.ts
603
- import * as fs2 from "fs/promises";
604
- import * as path2 from "path";
605
- import { fileURLToPath as fileURLToPath2 } from "url";
603
+ import * as fs3 from "fs/promises";
604
+ import * as path3 from "path";
605
+ import { fileURLToPath as fileURLToPath3 } from "url";
606
606
 
607
- // src/surfaces/serve-mcp-stdio.ts
607
+ // src/surfaces/connector-docs.ts
608
608
  import * as fs from "fs/promises";
609
609
  import * as path from "path";
610
610
  import { fileURLToPath } from "url";
611
+ var SKILL_FILENAME = "SKILL.md";
612
+ var REFERENCES_DIRNAME = "references";
613
+ function connectorDirFromMeta(meta) {
614
+ const dir = path.dirname(fileURLToPath(meta.url));
615
+ return path.basename(dir) === "dist" ? path.dirname(dir) : dir;
616
+ }
617
+ function skillPath(connectorDir) {
618
+ return path.join(connectorDir, SKILL_FILENAME);
619
+ }
620
+ function referencePath(connectorDir, name) {
621
+ return path.join(connectorDir, REFERENCES_DIRNAME, `${name}.md`);
622
+ }
623
+ async function loadSkillDoc(connectorDir) {
624
+ return loadDoc(skillPath(connectorDir), connectorDir);
625
+ }
626
+ async function listReferenceNames(connectorDir) {
627
+ try {
628
+ const entries = await fs.readdir(
629
+ path.join(connectorDir, REFERENCES_DIRNAME),
630
+ { recursive: true }
631
+ );
632
+ return entries.filter((entry) => entry.endsWith(".md")).map((entry) => toPosix(entry).slice(0, -".md".length));
633
+ } catch {
634
+ return [];
635
+ }
636
+ }
637
+ function toPosix(p) {
638
+ return p.split(path.sep).join("/");
639
+ }
640
+ async function loadReferenceDoc(connectorDir, name) {
641
+ return loadDoc(
642
+ referencePath(connectorDir, name),
643
+ path.join(connectorDir, REFERENCES_DIRNAME)
644
+ );
645
+ }
646
+ async function loadDoc(filePath, baseDir) {
647
+ let raw;
648
+ try {
649
+ raw = await fs.readFile(filePath, "utf8");
650
+ } catch {
651
+ return void 0;
652
+ }
653
+ return { path: filePath, content: resolveRelativeLinks(raw, baseDir) };
654
+ }
655
+ function resolveRelativeLinks(content, baseDir) {
656
+ return content.replace(
657
+ /(!?\[([^\]]*)\])\(([^)]+)\)/g,
658
+ (match, prefix, _alt, url) => {
659
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file:") || url.startsWith("#") || url.startsWith("/")) {
660
+ return match;
661
+ }
662
+ return `${prefix}(${path.resolve(baseDir, url)})`;
663
+ }
664
+ );
665
+ }
666
+
667
+ // src/surfaces/serve-mcp-stdio.ts
668
+ import * as fs2 from "fs/promises";
669
+ import * as path2 from "path";
670
+ import { fileURLToPath as fileURLToPath2 } from "url";
611
671
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
612
672
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
613
673
  function buildMcpRegistry(scripts, env, connectionResolvers) {
@@ -628,9 +688,9 @@ async function resolveServerInfo(meta) {
628
688
  return { name: FALLBACK_SERVER_NAME, version: FALLBACK_SERVER_VERSION };
629
689
  }
630
690
  try {
631
- const cliPath = fileURLToPath(meta.url);
632
- const pkgPath = path.join(path.dirname(cliPath), "package.json");
633
- const raw = await fs.readFile(pkgPath, "utf8");
691
+ const cliPath = fileURLToPath2(meta.url);
692
+ const pkgPath = path2.join(path2.dirname(cliPath), "package.json");
693
+ const raw = await fs2.readFile(pkgPath, "utf8");
634
694
  const pkg = JSON.parse(raw);
635
695
  return {
636
696
  name: typeof pkg.name === "string" ? pkg.name : FALLBACK_SERVER_NAME,
@@ -640,9 +700,8 @@ async function resolveServerInfo(meta) {
640
700
  return { name: FALLBACK_SERVER_NAME, version: FALLBACK_SERVER_VERSION };
641
701
  }
642
702
  }
643
- async function serveMcpStdio(meta, connector, opts = {}) {
703
+ async function buildMcpServer(meta, connector, opts = {}) {
644
704
  const env = opts.env ?? process.env;
645
- const stderr = opts.stderr ?? process.stderr;
646
705
  const scripts = connector.scripts;
647
706
  const connectionResolvers = connector.connectionResolvers;
648
707
  const registry = buildMcpRegistry(scripts, env, connectionResolvers);
@@ -666,10 +725,69 @@ async function serveMcpStdio(meta, connector, opts = {}) {
666
725
  }
667
726
  );
668
727
  }
728
+ if (meta.url) {
729
+ await registerDocResources(
730
+ server,
731
+ connectorSlug(serverInfo.name),
732
+ connectorDirFromMeta(meta)
733
+ );
734
+ }
735
+ return { server, serverInfo };
736
+ }
737
+ async function serveMcpStdio(meta, connector, opts = {}) {
738
+ const stderr = opts.stderr ?? process.stderr;
739
+ const { server, serverInfo } = await buildMcpServer(meta, connector, opts);
669
740
  await server.connect(new StdioServerTransport());
670
741
  stderr.write(`[${serverInfo.name}] MCP server ready on stdio.
671
742
  `);
672
743
  }
744
+ var MARKDOWN_MIME_TYPE = "text/markdown";
745
+ function connectorSlug(serverName) {
746
+ const basename2 = serverName.split("/").pop() ?? serverName;
747
+ return basename2.replace(/-connector$/, "") || basename2;
748
+ }
749
+ async function registerDocResources(server, slug, connectorDir) {
750
+ const skill = await loadSkillDoc(connectorDir);
751
+ if (skill) {
752
+ server.registerResource(
753
+ SKILL_FILENAME,
754
+ `connector://${slug}/${SKILL_FILENAME}`,
755
+ {
756
+ title: "Skill guide",
757
+ description: "The connector's SKILL.md: when to use it, auth setup, and the script catalog.",
758
+ mimeType: MARKDOWN_MIME_TYPE
759
+ },
760
+ readDoc(() => loadSkillDoc(connectorDir), skill.path)
761
+ );
762
+ }
763
+ for (const name of await listReferenceNames(connectorDir)) {
764
+ const resourceName = `${REFERENCES_DIRNAME}/${name}.md`;
765
+ const uriPath = name.split("/").map(encodeURIComponent).join("/");
766
+ server.registerResource(
767
+ resourceName,
768
+ `connector://${slug}/${REFERENCES_DIRNAME}/${uriPath}.md`,
769
+ {
770
+ title: `Reference: ${name}`,
771
+ description: `Durable per-app knowledge from references/${name}.md (API gotchas, encodings, edge cases).`,
772
+ mimeType: MARKDOWN_MIME_TYPE
773
+ },
774
+ readDoc(() => loadReferenceDoc(connectorDir, name), resourceName)
775
+ );
776
+ }
777
+ }
778
+ function readDoc(load, label) {
779
+ return async (uri) => {
780
+ const doc = await load();
781
+ if (!doc) {
782
+ throw new Error(`Resource no longer available: ${label}.`);
783
+ }
784
+ return {
785
+ contents: [
786
+ { uri: uri.href, mimeType: MARKDOWN_MIME_TYPE, text: doc.content }
787
+ ]
788
+ };
789
+ };
790
+ }
673
791
 
674
792
  // src/surfaces/run-dispatch-cli.ts
675
793
  function asAnyDefinition(value) {
@@ -739,18 +857,14 @@ async function runDispatchCliBody(connector, opts) {
739
857
  "runDispatchCliBody: `skill` requires `meta` in options (the connector's `import.meta` from `cli.ts`)."
740
858
  );
741
859
  }
742
- const cliPath = fileURLToPath2(opts.meta.url);
743
- const connectorDir = path2.dirname(cliPath);
744
- const skillPath = path2.join(connectorDir, "SKILL.md");
745
- let content;
746
- try {
747
- content = await fs2.readFile(skillPath, "utf8");
748
- } catch {
860
+ const connectorDir = connectorDirFromMeta(opts.meta);
861
+ const skill = await loadSkillDoc(connectorDir);
862
+ if (!skill) {
749
863
  throw new Error(
750
- `SKILL.md not found at ${skillPath}. Ensure the connector ships a SKILL.md.`
864
+ `SKILL.md not found at ${skillPath(connectorDir)}. Ensure the connector ships a SKILL.md.`
751
865
  );
752
866
  }
753
- opts.stdout.write(resolveRelativeLinks(content, connectorDir));
867
+ opts.stdout.write(skill.content);
754
868
  return;
755
869
  }
756
870
  if (first === "reference") {
@@ -759,18 +873,10 @@ async function runDispatchCliBody(connector, opts) {
759
873
  "runDispatchCliBody: `reference` requires `meta` in options (the connector's `import.meta` from `cli.ts`)."
760
874
  );
761
875
  }
762
- const cliPath = fileURLToPath2(opts.meta.url);
763
- const connectorDir = path2.dirname(cliPath);
764
- const refsDir = path2.join(connectorDir, "references");
876
+ const connectorDir = connectorDirFromMeta(opts.meta);
765
877
  const name = args[1];
766
878
  if (!name) {
767
- let entries;
768
- try {
769
- const files = await fs2.readdir(refsDir);
770
- entries = files.filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -".md".length));
771
- } catch {
772
- entries = [];
773
- }
879
+ const entries = await listReferenceNames(connectorDir);
774
880
  if (entries.length === 0) {
775
881
  opts.stdout.write(`Available references: <none>
776
882
  `);
@@ -784,16 +890,13 @@ async function runDispatchCliBody(connector, opts) {
784
890
  }
785
891
  return;
786
892
  }
787
- const refPath = path2.join(refsDir, `${name}.md`);
788
- let content;
789
- try {
790
- content = await fs2.readFile(refPath, "utf8");
791
- } catch {
893
+ const reference = await loadReferenceDoc(connectorDir, name);
894
+ if (!reference) {
792
895
  throw new Error(
793
- `Reference file not found at ${refPath}. Ensure the connector ships \`references/${name}.md\`.`
896
+ `Reference file not found at ${referencePath(connectorDir, name)}. Ensure the connector ships \`references/${name}.md\`.`
794
897
  );
795
898
  }
796
- opts.stdout.write(resolveRelativeLinks(content, refsDir));
899
+ opts.stdout.write(reference.content);
797
900
  return;
798
901
  }
799
902
  if (first !== "run") {
@@ -826,23 +929,12 @@ async function runDispatchCliBody(connector, opts) {
826
929
  invocation
827
930
  });
828
931
  }
829
- function resolveRelativeLinks(content, baseDir) {
830
- return content.replace(
831
- /(!?\[([^\]]*)\])\(([^)]+)\)/g,
832
- (match, prefix, _alt, url) => {
833
- if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file:") || url.startsWith("#") || url.startsWith("/")) {
834
- return match;
835
- }
836
- return `${prefix}(${path2.resolve(baseDir, url)})`;
837
- }
838
- );
839
- }
840
932
  async function resolvePackageName(meta) {
841
933
  if (!meta?.url) return void 0;
842
934
  try {
843
- const cliPath = fileURLToPath2(meta.url);
844
- const pkgPath = path2.join(path2.dirname(cliPath), "package.json");
845
- const raw = await fs2.readFile(pkgPath, "utf8");
935
+ const cliPath = fileURLToPath3(meta.url);
936
+ const pkgPath = path3.join(path3.dirname(cliPath), "package.json");
937
+ const raw = await fs3.readFile(pkgPath, "utf8");
846
938
  const pkg = JSON.parse(raw);
847
939
  return typeof pkg.name === "string" ? pkg.name : void 0;
848
940
  } catch {
@@ -852,8 +944,8 @@ async function resolvePackageName(meta) {
852
944
  function printUsage(scripts, out, packageName) {
853
945
  const names = Object.keys(scripts);
854
946
  out.write(
855
- `Usage: <bin> run <script> [<json-input>]
856
- <bin> run <script> --help (per-script input + required env vars)
947
+ `Usage: <bin> run <script> [<json-input>] [--filter <jq>]
948
+ <bin> run <script> --help (per-script input/output schema, env vars, --filter)
857
949
  <bin> mcp (run as a local MCP server over stdio)
858
950
  <bin> skill (print SKILL.md to stdout)
859
951
  <bin> reference [<name>] (print references/<name>.md, or list all)
@@ -882,7 +974,7 @@ function printUsage(scripts, out, packageName) {
882
974
  }
883
975
  out.write(
884
976
  `
885
- Credentials are environment-variable only \u2014 run \`<bin> run <script> --help\` to list each script's required env vars.
977
+ Run \`<bin> run <script> --help\` for a script's input/output schema, its required env vars (credentials are environment-variable only), and the \`--filter <jq>\` flag for trimming output.
886
978
  `
887
979
  );
888
980
  }
@@ -908,6 +1000,13 @@ env-only \u2014 set the env vars for each script you intend to call, listed
908
1000
  by \`<bin> run <script> --help\`. Resolution happens once per script at
909
1001
  server start; credentials never traverse the MCP transport.
910
1002
 
1003
+ `
1004
+ );
1005
+ out.write(
1006
+ `The connector's docs are exposed as MCP resources (text/markdown): its
1007
+ SKILL.md and each references/*.md file, mirroring \`<bin> skill\` and
1008
+ \`<bin> reference\`. Clients read them via resources/list + resources/read.
1009
+
911
1010
  `
912
1011
  );
913
1012
  const names = Object.keys(scripts);
@@ -951,12 +1050,31 @@ async function handleIfScriptMainBody(wrappedScript, connectionResolvers, io) {
951
1050
  const args = io.argv.slice(2);
952
1051
  let positional;
953
1052
  let helpRequested = false;
954
- for (const arg of args) {
1053
+ let filter;
1054
+ for (let i = 0; i < args.length; i++) {
1055
+ const arg = args[i];
955
1056
  if (arg === "--help" || arg === "-h") {
956
1057
  helpRequested = true;
1058
+ } else if (arg === "--filter") {
1059
+ const value = args[i + 1];
1060
+ if (value === void 0 || value.length === 0) {
1061
+ throw new Error(
1062
+ `\`--filter\` requires a jq expression, e.g. \`--filter '.results | length'\`.`
1063
+ );
1064
+ }
1065
+ filter = value;
1066
+ i++;
1067
+ } else if (arg.startsWith("--filter=")) {
1068
+ const value = arg.slice("--filter=".length);
1069
+ if (value.length === 0) {
1070
+ throw new Error(
1071
+ `\`--filter\` requires a jq expression, e.g. \`--filter '.results | length'\`.`
1072
+ );
1073
+ }
1074
+ filter = value;
957
1075
  } else if (arg.startsWith("--")) {
958
1076
  throw new Error(
959
- `Unknown flag "${arg}". The per-script CLI accepts only \`--help\` and a positional JSON-encoded input. Credentials are env-only \u2014 set the resolver's required env vars (run with \`--help\` to list them).`
1077
+ `Unknown flag "${arg}". The per-script CLI accepts \`--help\`, \`--filter <jq>\`, and a positional JSON-encoded input. Credentials are env-only \u2014 set the resolver's required env vars (run with \`--help\` to list them).`
960
1078
  );
961
1079
  } else if (positional === void 0) {
962
1080
  positional = arg;
@@ -1001,7 +1119,12 @@ async function handleIfScriptMainBody(wrappedScript, connectionResolvers, io) {
1001
1119
  }
1002
1120
  }
1003
1121
  const result = await wrappedScript.run(input, runOpts);
1004
- io.stdout.write(JSON.stringify(result, null, 2) + "\n");
1122
+ const output = filter === void 0 ? result : await applyJqFilter(result, filter);
1123
+ io.stdout.write(JSON.stringify(output, null, 2) + "\n");
1124
+ }
1125
+ async function applyJqFilter(value, filter) {
1126
+ const { json: runJq } = await import("jq-wasm");
1127
+ return runJq(value, filter);
1005
1128
  }
1006
1129
  async function readStdinInput(stdin, scriptName) {
1007
1130
  if (stdin.isTTY) {
@@ -1020,11 +1143,19 @@ function buildHelpText(definition, connectionResolvers, opts = {}) {
1020
1143
  lines.push("");
1021
1144
  lines.push("Usage:");
1022
1145
  if (opts.invocation) {
1023
- lines.push(` <bin> run ${opts.invocation.scriptName} '<json-input>'`);
1146
+ lines.push(
1147
+ ` <bin> run ${opts.invocation.scriptName} '<json-input>' [--filter <jq>]`
1148
+ );
1024
1149
  } else {
1025
- lines.push(` <bin> '<json-input>'`);
1150
+ lines.push(` <bin> '<json-input>' [--filter <jq>]`);
1026
1151
  }
1027
1152
  lines.push("");
1153
+ lines.push(
1154
+ " --filter <jq> Transform the JSON output through a jq expression."
1155
+ );
1156
+ lines.push(" Useful for trimming large outputs, e.g.");
1157
+ lines.push(" --filter '.results | length'.");
1158
+ lines.push("");
1028
1159
  if (opts.invocation) {
1029
1160
  lines.push("Example:");
1030
1161
  lines.push(
@@ -1049,6 +1180,11 @@ function buildHelpText(definition, connectionResolvers, opts = {}) {
1049
1180
  lines.push(...inputBlock);
1050
1181
  lines.push("");
1051
1182
  }
1183
+ const outputBlock = formatHelpForOutput(definition);
1184
+ if (outputBlock.length > 0) {
1185
+ lines.push(...outputBlock);
1186
+ lines.push("");
1187
+ }
1052
1188
  return lines.join("\n");
1053
1189
  }
1054
1190
  function formatExampleCommand(definition, connectionResolvers, invocation) {
@@ -1085,6 +1221,20 @@ function formatHelpForInput(definition) {
1085
1221
  }
1086
1222
  return lines;
1087
1223
  }
1224
+ function formatHelpForOutput(definition) {
1225
+ let schema;
1226
+ try {
1227
+ schema = z3.toJSONSchema(definition.outputSchema);
1228
+ } catch {
1229
+ return [];
1230
+ }
1231
+ const lines = ["Output (JSON Schema):"];
1232
+ const json = JSON.stringify(schema, null, 2);
1233
+ for (const line of json.split("\n")) {
1234
+ lines.push(` ${line}`);
1235
+ }
1236
+ return lines;
1237
+ }
1088
1238
 
1089
1239
  // src/surfaces/to-functions.ts
1090
1240
  function toFunctions(connector) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zapier/connectors-sdk",
3
- "version": "0.1.0-experimental.11",
3
+ "version": "0.1.0-experimental.13",
4
4
  "description": "SDK for building Zapier connectors. Provides the authoring primitives and execution surfaces for connector scripts.",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -24,6 +24,9 @@
24
24
  "README.md",
25
25
  "LICENSE"
26
26
  ],
27
+ "dependencies": {
28
+ "jq-wasm": "1.1.0-jq-1.8.1"
29
+ },
27
30
  "peerDependencies": {
28
31
  "@modelcontextprotocol/sdk": "^1.0.0",
29
32
  "@zapier/zapier-sdk": "^0.59.0",