@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.
- package/README.internal.md +12 -11
- package/dist/index.cjs +197 -49
- package/dist/index.js +197 -49
- package/package.json +1 -1
package/README.internal.md
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
677
|
-
var
|
|
678
|
-
var
|
|
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/
|
|
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,
|
|
705
|
-
const pkgPath =
|
|
706
|
-
const raw = await
|
|
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
|
|
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
|
|
816
|
-
const
|
|
817
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
861
|
-
|
|
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 ${
|
|
1002
|
+
`Reference file not found at ${referencePath(connectorDir, name)}. Ensure the connector ships \`references/${name}.md\`.`
|
|
867
1003
|
);
|
|
868
1004
|
}
|
|
869
|
-
opts.stdout.write(
|
|
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,
|
|
917
|
-
const pkgPath =
|
|
918
|
-
const raw = await
|
|
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
|
|
571
|
-
import * as
|
|
572
|
-
import { fileURLToPath as
|
|
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/
|
|
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 =
|
|
599
|
-
const pkgPath =
|
|
600
|
-
const raw = await
|
|
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
|
|
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
|
|
710
|
-
const
|
|
711
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
755
|
-
|
|
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 ${
|
|
896
|
+
`Reference file not found at ${referencePath(connectorDir, name)}. Ensure the connector ships \`references/${name}.md\`.`
|
|
761
897
|
);
|
|
762
898
|
}
|
|
763
|
-
opts.stdout.write(
|
|
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 =
|
|
811
|
-
const pkgPath =
|
|
812
|
-
const raw = await
|
|
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.
|
|
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",
|