@zapier/connectors-sdk 0.1.0-experimental.10 → 0.1.0-experimental.12

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
@@ -280,6 +280,39 @@ function isPackageInstalled(name) {
280
280
  return false;
281
281
  }
282
282
  }
283
+ function buildMissingConnectionError(scriptName, missingSlots) {
284
+ const lines = [];
285
+ if (missingSlots.length === 1 && missingSlots[0].slotName === void 0) {
286
+ const { connectionKey, resolvers } = missingSlots[0];
287
+ lines.push(
288
+ `"${scriptName}" requires the "${connectionKey}" connection but no credentials were found in the environment.`
289
+ );
290
+ lines.push("");
291
+ lines.push(
292
+ "Set one of the following environment variables before running:"
293
+ );
294
+ lines.push("");
295
+ for (const resolver of resolvers) {
296
+ const vars = envVarsFor(void 0, connectionKey, resolver);
297
+ lines.push(` ${vars.join(" ")} (${resolver.name})`);
298
+ }
299
+ } else {
300
+ lines.push(
301
+ `"${scriptName}" is missing credentials for connection slot(s):`
302
+ );
303
+ for (const { slotName, connectionKey, resolvers } of missingSlots) {
304
+ lines.push("");
305
+ lines.push(` ${slotName} (${connectionKey}):`);
306
+ for (const resolver of resolvers) {
307
+ const vars = envVarsFor(slotName, connectionKey, resolver);
308
+ lines.push(` ${vars.join(" ")} (${resolver.name})`);
309
+ }
310
+ }
311
+ }
312
+ lines.push("");
313
+ lines.push("Run with --help for the full auth guide.");
314
+ return new Error(lines.join("\n"));
315
+ }
283
316
  function formatHelpForConnections(definition, connectionResolvers, env) {
284
317
  const slots = [...walkConnections(definition)];
285
318
  if (slots.length === 0) return [];
@@ -673,14 +706,74 @@ function defineTool(config) {
673
706
  var import_zod3 = require("zod");
674
707
 
675
708
  // src/surfaces/run-dispatch-cli.ts
676
- var fs2 = __toESM(require("fs/promises"), 1);
677
- var path2 = __toESM(require("path"), 1);
678
- 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");
679
712
 
680
- // src/surfaces/serve-mcp-stdio.ts
713
+ // src/surfaces/connector-docs.ts
681
714
  var fs = __toESM(require("fs/promises"), 1);
682
715
  var path = __toESM(require("path"), 1);
683
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");
684
777
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
685
778
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
686
779
  function buildMcpRegistry(scripts, env, connectionResolvers) {
@@ -701,9 +794,9 @@ async function resolveServerInfo(meta) {
701
794
  return { name: FALLBACK_SERVER_NAME, version: FALLBACK_SERVER_VERSION };
702
795
  }
703
796
  try {
704
- const cliPath = (0, import_node_url.fileURLToPath)(meta.url);
705
- const pkgPath = path.join(path.dirname(cliPath), "package.json");
706
- 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");
707
800
  const pkg = JSON.parse(raw);
708
801
  return {
709
802
  name: typeof pkg.name === "string" ? pkg.name : FALLBACK_SERVER_NAME,
@@ -713,9 +806,8 @@ async function resolveServerInfo(meta) {
713
806
  return { name: FALLBACK_SERVER_NAME, version: FALLBACK_SERVER_VERSION };
714
807
  }
715
808
  }
716
- async function serveMcpStdio(meta, connector, opts = {}) {
809
+ async function buildMcpServer(meta, connector, opts = {}) {
717
810
  const env = opts.env ?? process.env;
718
- const stderr = opts.stderr ?? process.stderr;
719
811
  const scripts = connector.scripts;
720
812
  const connectionResolvers = connector.connectionResolvers;
721
813
  const registry = buildMcpRegistry(scripts, env, connectionResolvers);
@@ -739,10 +831,69 @@ async function serveMcpStdio(meta, connector, opts = {}) {
739
831
  }
740
832
  );
741
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);
742
846
  await server.connect(new import_stdio.StdioServerTransport());
743
847
  stderr.write(`[${serverInfo.name}] MCP server ready on stdio.
744
848
  `);
745
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
+ }
746
897
 
747
898
  // src/surfaces/run-dispatch-cli.ts
748
899
  function asAnyDefinition(value) {
@@ -812,18 +963,14 @@ async function runDispatchCliBody(connector, opts) {
812
963
  "runDispatchCliBody: `skill` requires `meta` in options (the connector's `import.meta` from `cli.ts`)."
813
964
  );
814
965
  }
815
- const cliPath = (0, import_node_url2.fileURLToPath)(opts.meta.url);
816
- const connectorDir = path2.dirname(cliPath);
817
- const skillPath = path2.join(connectorDir, "SKILL.md");
818
- let content;
819
- try {
820
- content = await fs2.readFile(skillPath, "utf8");
821
- } catch {
966
+ const connectorDir = connectorDirFromMeta(opts.meta);
967
+ const skill = await loadSkillDoc(connectorDir);
968
+ if (!skill) {
822
969
  throw new Error(
823
- `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.`
824
971
  );
825
972
  }
826
- opts.stdout.write(resolveRelativeLinks(content, connectorDir));
973
+ opts.stdout.write(skill.content);
827
974
  return;
828
975
  }
829
976
  if (first === "reference") {
@@ -832,18 +979,10 @@ async function runDispatchCliBody(connector, opts) {
832
979
  "runDispatchCliBody: `reference` requires `meta` in options (the connector's `import.meta` from `cli.ts`)."
833
980
  );
834
981
  }
835
- const cliPath = (0, import_node_url2.fileURLToPath)(opts.meta.url);
836
- const connectorDir = path2.dirname(cliPath);
837
- const refsDir = path2.join(connectorDir, "references");
982
+ const connectorDir = connectorDirFromMeta(opts.meta);
838
983
  const name = args[1];
839
984
  if (!name) {
840
- let entries;
841
- try {
842
- const files = await fs2.readdir(refsDir);
843
- entries = files.filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -".md".length));
844
- } catch {
845
- entries = [];
846
- }
985
+ const entries = await listReferenceNames(connectorDir);
847
986
  if (entries.length === 0) {
848
987
  opts.stdout.write(`Available references: <none>
849
988
  `);
@@ -857,16 +996,13 @@ async function runDispatchCliBody(connector, opts) {
857
996
  }
858
997
  return;
859
998
  }
860
- const refPath = path2.join(refsDir, `${name}.md`);
861
- let content;
862
- try {
863
- content = await fs2.readFile(refPath, "utf8");
864
- } catch {
999
+ const reference = await loadReferenceDoc(connectorDir, name);
1000
+ if (!reference) {
865
1001
  throw new Error(
866
- `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\`.`
867
1003
  );
868
1004
  }
869
- opts.stdout.write(resolveRelativeLinks(content, refsDir));
1005
+ opts.stdout.write(reference.content);
870
1006
  return;
871
1007
  }
872
1008
  if (first !== "run") {
@@ -899,23 +1035,12 @@ async function runDispatchCliBody(connector, opts) {
899
1035
  invocation
900
1036
  });
901
1037
  }
902
- function resolveRelativeLinks(content, baseDir) {
903
- return content.replace(
904
- /(!?\[([^\]]*)\])\(([^)]+)\)/g,
905
- (match, prefix, _alt, url) => {
906
- if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file:") || url.startsWith("#") || url.startsWith("/")) {
907
- return match;
908
- }
909
- return `${prefix}(${path2.resolve(baseDir, url)})`;
910
- }
911
- );
912
- }
913
1038
  async function resolvePackageName(meta) {
914
1039
  if (!meta?.url) return void 0;
915
1040
  try {
916
- const cliPath = (0, import_node_url2.fileURLToPath)(meta.url);
917
- const pkgPath = path2.join(path2.dirname(cliPath), "package.json");
918
- 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");
919
1044
  const pkg = JSON.parse(raw);
920
1045
  return typeof pkg.name === "string" ? pkg.name : void 0;
921
1046
  } catch {
@@ -981,6 +1106,13 @@ env-only \u2014 set the env vars for each script you intend to call, listed
981
1106
  by \`<bin> run <script> --help\`. Resolution happens once per script at
982
1107
  server start; credentials never traverse the MCP transport.
983
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
+
984
1116
  `
985
1117
  );
986
1118
  const names = Object.keys(scripts);
@@ -1057,6 +1189,22 @@ async function handleIfScriptMainBody(wrappedScript, connectionResolvers, io) {
1057
1189
  const input = wrappedScript.inputSchema.parse(JSON.parse(raw));
1058
1190
  const hasConnections = [...walkConnections(wrappedScript)].length > 0;
1059
1191
  const runOpts = hasConnections ? buildRunOptionsFromEnv(wrappedScript, io.env, connectionResolvers) : void 0;
1192
+ if (runOpts !== void 0) {
1193
+ const missing = [];
1194
+ for (const { slotName, connectionKey } of walkConnections(wrappedScript)) {
1195
+ const value = slotName === void 0 ? "connection" in runOpts ? runOpts.connection : void 0 : "connections" in runOpts ? runOpts.connections[slotName] : void 0;
1196
+ if (value === void 0) {
1197
+ missing.push({
1198
+ slotName,
1199
+ connectionKey,
1200
+ resolvers: resolversForKey(connectionResolvers, connectionKey)
1201
+ });
1202
+ }
1203
+ }
1204
+ if (missing.length > 0) {
1205
+ throw buildMissingConnectionError(wrappedScript.name, missing);
1206
+ }
1207
+ }
1060
1208
  const result = await wrappedScript.run(input, runOpts);
1061
1209
  io.stdout.write(JSON.stringify(result, null, 2) + "\n");
1062
1210
  }
package/dist/index.js CHANGED
@@ -174,6 +174,39 @@ function isPackageInstalled(name) {
174
174
  return false;
175
175
  }
176
176
  }
177
+ function buildMissingConnectionError(scriptName, missingSlots) {
178
+ const lines = [];
179
+ if (missingSlots.length === 1 && missingSlots[0].slotName === void 0) {
180
+ const { connectionKey, resolvers } = missingSlots[0];
181
+ lines.push(
182
+ `"${scriptName}" requires the "${connectionKey}" connection but no credentials were found in the environment.`
183
+ );
184
+ lines.push("");
185
+ lines.push(
186
+ "Set one of the following environment variables before running:"
187
+ );
188
+ lines.push("");
189
+ for (const resolver of resolvers) {
190
+ const vars = envVarsFor(void 0, connectionKey, resolver);
191
+ lines.push(` ${vars.join(" ")} (${resolver.name})`);
192
+ }
193
+ } else {
194
+ lines.push(
195
+ `"${scriptName}" is missing credentials for connection slot(s):`
196
+ );
197
+ for (const { slotName, connectionKey, resolvers } of missingSlots) {
198
+ lines.push("");
199
+ lines.push(` ${slotName} (${connectionKey}):`);
200
+ for (const resolver of resolvers) {
201
+ const vars = envVarsFor(slotName, connectionKey, resolver);
202
+ lines.push(` ${vars.join(" ")} (${resolver.name})`);
203
+ }
204
+ }
205
+ }
206
+ lines.push("");
207
+ lines.push("Run with --help for the full auth guide.");
208
+ return new Error(lines.join("\n"));
209
+ }
177
210
  function formatHelpForConnections(definition, connectionResolvers, env) {
178
211
  const slots = [...walkConnections(definition)];
179
212
  if (slots.length === 0) return [];
@@ -567,14 +600,74 @@ function defineTool(config) {
567
600
  import { z as z3 } from "zod";
568
601
 
569
602
  // src/surfaces/run-dispatch-cli.ts
570
- import * as fs2 from "fs/promises";
571
- import * as path2 from "path";
572
- 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";
573
606
 
574
- // src/surfaces/serve-mcp-stdio.ts
607
+ // src/surfaces/connector-docs.ts
575
608
  import * as fs from "fs/promises";
576
609
  import * as path from "path";
577
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";
578
671
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
579
672
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
580
673
  function buildMcpRegistry(scripts, env, connectionResolvers) {
@@ -595,9 +688,9 @@ async function resolveServerInfo(meta) {
595
688
  return { name: FALLBACK_SERVER_NAME, version: FALLBACK_SERVER_VERSION };
596
689
  }
597
690
  try {
598
- const cliPath = fileURLToPath(meta.url);
599
- const pkgPath = path.join(path.dirname(cliPath), "package.json");
600
- 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");
601
694
  const pkg = JSON.parse(raw);
602
695
  return {
603
696
  name: typeof pkg.name === "string" ? pkg.name : FALLBACK_SERVER_NAME,
@@ -607,9 +700,8 @@ async function resolveServerInfo(meta) {
607
700
  return { name: FALLBACK_SERVER_NAME, version: FALLBACK_SERVER_VERSION };
608
701
  }
609
702
  }
610
- async function serveMcpStdio(meta, connector, opts = {}) {
703
+ async function buildMcpServer(meta, connector, opts = {}) {
611
704
  const env = opts.env ?? process.env;
612
- const stderr = opts.stderr ?? process.stderr;
613
705
  const scripts = connector.scripts;
614
706
  const connectionResolvers = connector.connectionResolvers;
615
707
  const registry = buildMcpRegistry(scripts, env, connectionResolvers);
@@ -633,10 +725,69 @@ async function serveMcpStdio(meta, connector, opts = {}) {
633
725
  }
634
726
  );
635
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);
636
740
  await server.connect(new StdioServerTransport());
637
741
  stderr.write(`[${serverInfo.name}] MCP server ready on stdio.
638
742
  `);
639
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
+ }
640
791
 
641
792
  // src/surfaces/run-dispatch-cli.ts
642
793
  function asAnyDefinition(value) {
@@ -706,18 +857,14 @@ async function runDispatchCliBody(connector, opts) {
706
857
  "runDispatchCliBody: `skill` requires `meta` in options (the connector's `import.meta` from `cli.ts`)."
707
858
  );
708
859
  }
709
- const cliPath = fileURLToPath2(opts.meta.url);
710
- const connectorDir = path2.dirname(cliPath);
711
- const skillPath = path2.join(connectorDir, "SKILL.md");
712
- let content;
713
- try {
714
- content = await fs2.readFile(skillPath, "utf8");
715
- } catch {
860
+ const connectorDir = connectorDirFromMeta(opts.meta);
861
+ const skill = await loadSkillDoc(connectorDir);
862
+ if (!skill) {
716
863
  throw new Error(
717
- `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.`
718
865
  );
719
866
  }
720
- opts.stdout.write(resolveRelativeLinks(content, connectorDir));
867
+ opts.stdout.write(skill.content);
721
868
  return;
722
869
  }
723
870
  if (first === "reference") {
@@ -726,18 +873,10 @@ async function runDispatchCliBody(connector, opts) {
726
873
  "runDispatchCliBody: `reference` requires `meta` in options (the connector's `import.meta` from `cli.ts`)."
727
874
  );
728
875
  }
729
- const cliPath = fileURLToPath2(opts.meta.url);
730
- const connectorDir = path2.dirname(cliPath);
731
- const refsDir = path2.join(connectorDir, "references");
876
+ const connectorDir = connectorDirFromMeta(opts.meta);
732
877
  const name = args[1];
733
878
  if (!name) {
734
- let entries;
735
- try {
736
- const files = await fs2.readdir(refsDir);
737
- entries = files.filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -".md".length));
738
- } catch {
739
- entries = [];
740
- }
879
+ const entries = await listReferenceNames(connectorDir);
741
880
  if (entries.length === 0) {
742
881
  opts.stdout.write(`Available references: <none>
743
882
  `);
@@ -751,16 +890,13 @@ async function runDispatchCliBody(connector, opts) {
751
890
  }
752
891
  return;
753
892
  }
754
- const refPath = path2.join(refsDir, `${name}.md`);
755
- let content;
756
- try {
757
- content = await fs2.readFile(refPath, "utf8");
758
- } catch {
893
+ const reference = await loadReferenceDoc(connectorDir, name);
894
+ if (!reference) {
759
895
  throw new Error(
760
- `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\`.`
761
897
  );
762
898
  }
763
- opts.stdout.write(resolveRelativeLinks(content, refsDir));
899
+ opts.stdout.write(reference.content);
764
900
  return;
765
901
  }
766
902
  if (first !== "run") {
@@ -793,23 +929,12 @@ async function runDispatchCliBody(connector, opts) {
793
929
  invocation
794
930
  });
795
931
  }
796
- function resolveRelativeLinks(content, baseDir) {
797
- return content.replace(
798
- /(!?\[([^\]]*)\])\(([^)]+)\)/g,
799
- (match, prefix, _alt, url) => {
800
- if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file:") || url.startsWith("#") || url.startsWith("/")) {
801
- return match;
802
- }
803
- return `${prefix}(${path2.resolve(baseDir, url)})`;
804
- }
805
- );
806
- }
807
932
  async function resolvePackageName(meta) {
808
933
  if (!meta?.url) return void 0;
809
934
  try {
810
- const cliPath = fileURLToPath2(meta.url);
811
- const pkgPath = path2.join(path2.dirname(cliPath), "package.json");
812
- 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");
813
938
  const pkg = JSON.parse(raw);
814
939
  return typeof pkg.name === "string" ? pkg.name : void 0;
815
940
  } catch {
@@ -875,6 +1000,13 @@ env-only \u2014 set the env vars for each script you intend to call, listed
875
1000
  by \`<bin> run <script> --help\`. Resolution happens once per script at
876
1001
  server start; credentials never traverse the MCP transport.
877
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
+
878
1010
  `
879
1011
  );
880
1012
  const names = Object.keys(scripts);
@@ -951,6 +1083,22 @@ async function handleIfScriptMainBody(wrappedScript, connectionResolvers, io) {
951
1083
  const input = wrappedScript.inputSchema.parse(JSON.parse(raw));
952
1084
  const hasConnections = [...walkConnections(wrappedScript)].length > 0;
953
1085
  const runOpts = hasConnections ? buildRunOptionsFromEnv(wrappedScript, io.env, connectionResolvers) : void 0;
1086
+ if (runOpts !== void 0) {
1087
+ const missing = [];
1088
+ for (const { slotName, connectionKey } of walkConnections(wrappedScript)) {
1089
+ const value = slotName === void 0 ? "connection" in runOpts ? runOpts.connection : void 0 : "connections" in runOpts ? runOpts.connections[slotName] : void 0;
1090
+ if (value === void 0) {
1091
+ missing.push({
1092
+ slotName,
1093
+ connectionKey,
1094
+ resolvers: resolversForKey(connectionResolvers, connectionKey)
1095
+ });
1096
+ }
1097
+ }
1098
+ if (missing.length > 0) {
1099
+ throw buildMissingConnectionError(wrappedScript.name, missing);
1100
+ }
1101
+ }
954
1102
  const result = await wrappedScript.run(input, runOpts);
955
1103
  io.stdout.write(JSON.stringify(result, null, 2) + "\n");
956
1104
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zapier/connectors-sdk",
3
- "version": "0.1.0-experimental.10",
3
+ "version": "0.1.0-experimental.12",
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",