@zapier/connectors-sdk 0.1.0-experimental.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +93 -0
- package/README.internal.md +397 -0
- package/README.md +6 -0
- package/dist/build-zapier-fetch-DWCYBAF4.js +52 -0
- package/dist/index.cjs +1039 -0
- package/dist/index.d.cts +575 -0
- package/dist/index.d.ts +575 -0
- package/dist/index.js +933 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Elastic License 2.0
|
|
2
|
+
|
|
3
|
+
URL: https://www.elastic.co/licensing/elastic-license
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
8
|
+
|
|
9
|
+
## Copyright License
|
|
10
|
+
|
|
11
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide,
|
|
12
|
+
non-sublicensable, non-transferable license to use, copy, distribute, make
|
|
13
|
+
available, and prepare derivative works of the software, in each case subject to
|
|
14
|
+
the limitations and conditions below.
|
|
15
|
+
|
|
16
|
+
## Limitations
|
|
17
|
+
|
|
18
|
+
You may not provide the software to third parties as a hosted or managed
|
|
19
|
+
service, where the service provides users with access to any substantial set of
|
|
20
|
+
the features or functionality of the software.
|
|
21
|
+
|
|
22
|
+
You may not move, change, disable, or circumvent the license key functionality
|
|
23
|
+
in the software, and you may not remove or obscure any functionality in the
|
|
24
|
+
software that is protected by the license key.
|
|
25
|
+
|
|
26
|
+
You may not alter, remove, or obscure any licensing, copyright, or other notices
|
|
27
|
+
of the licensor in the software. Any use of the licensor's trademarks is subject
|
|
28
|
+
to applicable law.
|
|
29
|
+
|
|
30
|
+
## Patents
|
|
31
|
+
|
|
32
|
+
The licensor grants you a license, under any patent claims the licensor can
|
|
33
|
+
license, or becomes able to license, to make, have made, use, sell, offer for
|
|
34
|
+
sale, import and have imported the software, in each case subject to the
|
|
35
|
+
limitations and conditions in this license. This license does not cover any
|
|
36
|
+
patent claims that you cause to be infringed by modifications or additions to
|
|
37
|
+
the software. If you or your company make any written claim that the software
|
|
38
|
+
infringes or contributes to infringement of any patent, your patent license for
|
|
39
|
+
the software granted under these terms ends immediately. If your company makes
|
|
40
|
+
such a claim, your patent license ends immediately for work on behalf of your
|
|
41
|
+
company.
|
|
42
|
+
|
|
43
|
+
## Notices
|
|
44
|
+
|
|
45
|
+
You must ensure that anyone who gets a copy of any part of the software from you
|
|
46
|
+
also gets a copy of these terms.
|
|
47
|
+
|
|
48
|
+
If you modify the software, you must include in any modified copies of the
|
|
49
|
+
software prominent notices stating that you have modified the software.
|
|
50
|
+
|
|
51
|
+
## No Other Rights
|
|
52
|
+
|
|
53
|
+
These terms do not imply any licenses other than those expressly granted in
|
|
54
|
+
these terms.
|
|
55
|
+
|
|
56
|
+
## Termination
|
|
57
|
+
|
|
58
|
+
If you use the software in violation of these terms, such use is not licensed,
|
|
59
|
+
and your licenses will automatically terminate. If the licensor provides you
|
|
60
|
+
with a notice of your violation, and you cease all violation of this license no
|
|
61
|
+
later than 30 days after you receive that notice, your licenses will be
|
|
62
|
+
reinstated retroactively. However, if you violate these terms after such
|
|
63
|
+
reinstatement, any additional violation of these terms will cause your licenses
|
|
64
|
+
to terminate automatically and permanently.
|
|
65
|
+
|
|
66
|
+
## No Liability
|
|
67
|
+
|
|
68
|
+
*As far as the law allows, the software comes as is, without any warranty or
|
|
69
|
+
condition, and the licensor will not be liable to you for any damages arising
|
|
70
|
+
out of these terms or the use or nature of the software, under any kind of
|
|
71
|
+
legal claim.*
|
|
72
|
+
|
|
73
|
+
## Definitions
|
|
74
|
+
|
|
75
|
+
The **licensor** is the entity offering these terms, and the **software** is the
|
|
76
|
+
software the licensor makes available under these terms, including any portion
|
|
77
|
+
of it.
|
|
78
|
+
|
|
79
|
+
**you** refers to the individual or entity agreeing to these terms.
|
|
80
|
+
|
|
81
|
+
**your company** is any legal entity, sole proprietorship, or other kind of
|
|
82
|
+
organization that you work for, plus all organizations that have control over,
|
|
83
|
+
are under the control of, or are under common control with that
|
|
84
|
+
organization. **control** means ownership of substantially all the assets of an
|
|
85
|
+
entity, or the power to direct its management and policies by vote, contract, or
|
|
86
|
+
otherwise. Control can be direct or indirect.
|
|
87
|
+
|
|
88
|
+
**your licenses** are all the licenses granted to you for the software under
|
|
89
|
+
these terms.
|
|
90
|
+
|
|
91
|
+
**use** means anything you do with the software requiring one of your licenses.
|
|
92
|
+
|
|
93
|
+
**trademark** means trademarks, service marks, and similar rights.
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# @zapier/connectors-sdk
|
|
2
|
+
|
|
3
|
+
The workspace package every connector script imports from at the top of its module.
|
|
4
|
+
|
|
5
|
+
### Package entry (`index.ts`)
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
defineConnector,
|
|
10
|
+
defineConnectionResolver,
|
|
11
|
+
defineBearerTokenResolver,
|
|
12
|
+
defineTool,
|
|
13
|
+
handleIfScriptMain,
|
|
14
|
+
runDispatchCli,
|
|
15
|
+
toFunctions,
|
|
16
|
+
zapierConnectionResolver,
|
|
17
|
+
type AnyToolDefinition,
|
|
18
|
+
type ConnectorDefinition,
|
|
19
|
+
type RunOptions,
|
|
20
|
+
} from "@zapier/connectors-sdk";
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Export | Lives in | Purpose |
|
|
24
|
+
| ------------------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| **`defineTool(config)`** | `./define-tool.ts` | Authoring helper. Returns a `ToolDefinition` (none / single / multi connection). The author's `run` is `(input, ctx)` for connection-bearing scripts; `(input)` for credential-free ones. |
|
|
26
|
+
| **`defineConnector({ scripts, connectionResolvers })`** | `./define-connector.ts` | Connector `index.ts` helper. Wraps each script's `run` so consumers call `script.run(input, opts: RunOptions)`; the connector's resolvers translate `RunOptions` into the per-slot authed `fetch` the author's `run` receives via `ctx`. |
|
|
27
|
+
| **`defineConnectionResolver(resolver)`** | `./connection-resolvers.ts` | Typed-identity helper for hand-rolled resolvers; preserves the literal `name` and `keySuffixes` so `RunOptions.connection` narrows on the consumer side. |
|
|
28
|
+
| **`zapierConnectionResolver`** | `./connection-resolvers.ts` | SDK-shipped resolver (`name: "zapier-connection-id"`). Operators set `<KEY>_ZAPIER_CONNECTION_ID`; the resolver routes through Zapier Relay (lazy `import("@zapier/zapier-sdk")`). |
|
|
29
|
+
| **`defineBearerTokenResolver(opts?)`** | `./connection-resolvers.ts` | SDK-shipped resolver factory (default `name: "token"`, `scheme: "Bearer"`). Composes to `<KEY>_TOKEN` — matching the dominant ecosystem convention (`NOTION_TOKEN`, `GITHUB_TOKEN`). |
|
|
30
|
+
| **`toFunctions(connector)`** | `./surfaces/to-functions.ts` | Maps each script in a connector to its wrapped `.run` for package named exports. |
|
|
31
|
+
| **`handleIfScriptMain(meta, definition, opts?)`** | `./surfaces/handle-if-script-main.ts` | Per-script execution surface. When `import.meta.main`, parses argv/stdin, builds `RunOptions` from env via the supplied resolvers, calls the wrapped run. |
|
|
32
|
+
| **`runDispatchCli(meta, connector)`** | `./surfaces/run-dispatch-cli.ts` | Bundled-CLI surface. Routes `<bin> run <script> [args...]` to per-script CLI orchestration; `<bin> mcp` boots a local MCP stdio server exposing every script. |
|
|
33
|
+
| **Types** | `./tool-types.ts` / `./define-connector.ts` | `AnyToolDefinition`, `ConnectorDefinition`, `RunOptions`. Narrower types (`SingleConnectionResolver<TName>`, `MultiConnectionResolver<TName, TSuffixes>`, `Context*`, `RunOptions*`, …) are inferred via `defineTool` / `defineConnectionResolver` and aren't part of the public surface — polymorphic walks use `AnyToolDefinition`. |
|
|
34
|
+
|
|
35
|
+
### On the connector object (`defineConnector` return value)
|
|
36
|
+
|
|
37
|
+
| Member | Lives in | Purpose |
|
|
38
|
+
| ----------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| **`connector.scripts`** | — | Wrapped scripts: each `script.run(input, opts: RunOptions)` translates the caller-provided `ConnectionValue` into an authed `fetch` per slot via the connector's resolvers. |
|
|
40
|
+
| **`connector.connectionResolvers`** | — | The resolver map this connector was constructed with. X-app connectors will import resolver values from app connectors and re-register them under different connection keys. |
|
|
41
|
+
| **`toMcpTool(script)`** | `./surfaces/to-mcp-tool.ts` | Wire-format MCP `Tool` descriptor (JSON Schema'd `inputSchema`). For deprecated `Server` + `setRequestHandler` flows. |
|
|
42
|
+
| **`toMcpServerTool(script)`** | `./surfaces/to-mcp-server-tool.ts` | `McpServer.registerTool(name, config, cb)` config object (Zod schemas pass-through). For the modern API. |
|
|
43
|
+
| **`toChatCompletionTool(script)`** | `./surfaces/to-chat-completion-tool.ts` | Chat Completions registration shape. |
|
|
44
|
+
| **`toResponsesTool(script)`** | `./surfaces/to-responses-tool.ts` | Responses API registration shape. |
|
|
45
|
+
| **`buildRunOptionsFromEnv(script, env)`** | `./normalize-connections.ts` | Bound to the connector's resolvers. Build `RunOptions` for a wrapped script from a process-env bag — useful for long-running consumers that resolve credentials once and reuse the same opts for the life of the process. |
|
|
46
|
+
|
|
47
|
+
### Surfaces
|
|
48
|
+
|
|
49
|
+
A **surface** is how a `ToolDefinition` is exposed to a consumer. **Registration surfaces** publish tool metadata; **execution surfaces** run a tool.
|
|
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 |
|
|
61
|
+
|
|
62
|
+
## Authoring a script — `defineTool`
|
|
63
|
+
|
|
64
|
+
A script declares **whether it needs credentials** through a single string knob:
|
|
65
|
+
|
|
66
|
+
| Author writes | Means |
|
|
67
|
+
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `connection: "notion"` | One slot bound to connection key `notion`. The author's `run(input, ctx)` receives `ctx.fetch`. |
|
|
69
|
+
| `connections: { source: "notion", target: "notion" }` | Multiple slots, each bound to a connection key. The author's `run(input, ctx)` receives `ctx.connections.<slot>`. |
|
|
70
|
+
| (no `connection` / `connections` field) | Credential-free. The author's `run(input)` takes only the parsed input. |
|
|
71
|
+
|
|
72
|
+
The connection key is a string identifier — connectors register a resolver (or array of resolvers) for each key on the `connectionResolvers` map at `defineConnector` time. Authors don't see env-var names, programmatic-bag keys, slot prefixes, or Relay app slugs in their script body — the wrapper composes those from the resolver's `name` (and `keySuffixes` for multi-credential resolvers).
|
|
73
|
+
|
|
74
|
+
### Authoring example — single connection
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { z } from "zod";
|
|
78
|
+
import { defineTool, handleIfScriptMain } from "@zapier/connectors-sdk";
|
|
79
|
+
|
|
80
|
+
import { connectionResolvers } from "../connections.ts";
|
|
81
|
+
|
|
82
|
+
const definition = defineTool({
|
|
83
|
+
name: "search",
|
|
84
|
+
title: "Search Notion",
|
|
85
|
+
description: "Search Notion pages and databases by query string.",
|
|
86
|
+
inputSchema: z.object({ query: z.string() }),
|
|
87
|
+
outputSchema: z.object({ results: z.array(z.unknown()) }),
|
|
88
|
+
annotations: { readOnlyHint: true },
|
|
89
|
+
connection: "notion",
|
|
90
|
+
run: async (input, ctx) => {
|
|
91
|
+
const res = await ctx.fetch("https://api.notion.com/v1/search", {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: {
|
|
94
|
+
"Notion-Version": "2022-06-28",
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify(input),
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok)
|
|
100
|
+
throw new Error(`Notion search ${res.status}: ${await res.text()}`);
|
|
101
|
+
return res.json() as Promise<{ results: unknown[] }>;
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export default definition;
|
|
106
|
+
|
|
107
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Authoring example — multi-connection
|
|
111
|
+
|
|
112
|
+
A script that copies a page from one Notion workspace to another declares two slots (`source` + `target`) — same script body, two independently authed fetches. Both slots share the connection key `notion`; the wrapper distinguishes them by env-var prefix at run-time.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const definition = defineTool({
|
|
116
|
+
name: "copy_page",
|
|
117
|
+
description: "Copy a Notion page between two workspaces.",
|
|
118
|
+
inputSchema: z.object({
|
|
119
|
+
sourcePageId: z.string(),
|
|
120
|
+
targetParentPageId: z.string(),
|
|
121
|
+
}),
|
|
122
|
+
outputSchema: z.object({ id: z.string(), url: z.string() }),
|
|
123
|
+
connections: { source: "notion", target: "notion" },
|
|
124
|
+
run: async (input, ctx) => {
|
|
125
|
+
const page = await (
|
|
126
|
+
await ctx.connections.source(
|
|
127
|
+
`https://api.notion.com/v1/pages/${input.sourcePageId}`,
|
|
128
|
+
)
|
|
129
|
+
).json();
|
|
130
|
+
const created = await (
|
|
131
|
+
await ctx.connections.target("https://api.notion.com/v1/pages", {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { "Content-Type": "application/json" },
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
parent: { page_id: input.targetParentPageId },
|
|
136
|
+
properties: (page as { properties: unknown }).properties,
|
|
137
|
+
}),
|
|
138
|
+
})
|
|
139
|
+
).json();
|
|
140
|
+
return created as { id: string; url: string };
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Resolvers — bridging credentials and `fetch`
|
|
146
|
+
|
|
147
|
+
A **connection resolver** is a small data object the connector exports for one connection key. It declares:
|
|
148
|
+
|
|
149
|
+
- a `name` (e.g. `"token"`, `"zapier-connection-id"`, `"hmac"`),
|
|
150
|
+
- optional `keySuffixes` (for multi-credential shapes like HMAC `KEY` / `SECRET`),
|
|
151
|
+
- a `resolve(credential)` function that returns an authed `fetch`.
|
|
152
|
+
|
|
153
|
+
Resolvers don't know about env vars, slot names, or connection keys — the wrapper composes those at the boundary.
|
|
154
|
+
|
|
155
|
+
Every connector follows the same uniform shape: `connections.ts` exports a single keyed map named **`connectionResolvers`** (one entry per connection key the connector serves); `index.ts` and each script consume it as-is. No wrapping or re-keying is needed — the connector's connection keys are baked into the map.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
// apps/notion/connections.ts
|
|
159
|
+
import {
|
|
160
|
+
defineBearerTokenResolver,
|
|
161
|
+
zapierConnectionResolver,
|
|
162
|
+
} from "@zapier/connectors-sdk";
|
|
163
|
+
|
|
164
|
+
export const connectionResolvers = {
|
|
165
|
+
notion: [zapierConnectionResolver, defineBearerTokenResolver()],
|
|
166
|
+
} as const;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// apps/notion/index.ts
|
|
171
|
+
import { defineConnector, toFunctions } from "@zapier/connectors-sdk";
|
|
172
|
+
|
|
173
|
+
import { connectionResolvers } from "./connections.ts";
|
|
174
|
+
import searchDefinition from "./scripts/search.ts";
|
|
175
|
+
// ...
|
|
176
|
+
|
|
177
|
+
const connector = defineConnector({
|
|
178
|
+
scripts: { search: searchDefinition /* ... */ },
|
|
179
|
+
connectionResolvers,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
export default connector;
|
|
183
|
+
export const { search /* ... */ } = toFunctions(connector);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Multi-slot scripts (e.g. `connections: { source: "notion", target: "notion" }`) share a single `notion` entry — the wrapper routes each slot via its env-var prefix (`SOURCE_NOTION_*` vs `TARGET_NOTION_*`).
|
|
187
|
+
|
|
188
|
+
A downstream cross-app connector reaches the resolver list through `connector.connectionResolvers.<connectionKey>` and can re-register it under a different key:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import notion from "@zapier/notion-connector";
|
|
192
|
+
|
|
193
|
+
const notionResolvers = notion.connectionResolvers.notion;
|
|
194
|
+
|
|
195
|
+
defineConnector({
|
|
196
|
+
scripts: {
|
|
197
|
+
/* ... */
|
|
198
|
+
},
|
|
199
|
+
connectionResolvers: {
|
|
200
|
+
"notion-prod": notionResolvers,
|
|
201
|
+
"notion-staging": notionResolvers,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The order of resolvers in the array is also the **dispatch order**: at runtime, the wrapper picks the **first** resolver whose required env vars (or programmatic-bag keys) are all set. So putting `zapierConnectionResolver` before the bearer-token resolver means a connection ID wins when both are configured — useful for migration paths.
|
|
207
|
+
|
|
208
|
+
### SDK-shipped resolvers
|
|
209
|
+
|
|
210
|
+
| Helper | Shape | Composition (single-slot, key `notion`) |
|
|
211
|
+
| --------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
212
|
+
| **`zapierConnectionResolver`** | Single-credential value (no parameters). `name: "zapier-connection-id"`. | env: `NOTION_ZAPIER_CONNECTION_ID` · programmatic: `{ ZAPIER_CONNECTION_ID: string }` |
|
|
213
|
+
| **`defineBearerTokenResolver()`** | Single-credential factory. Default `name: "token"`, `scheme: "Bearer"`. Override either via `{ name, scheme }`. | env: `NOTION_TOKEN` · programmatic: `{ TOKEN: string }`. Override `name: "bearer-token"` for `<KEY>_BEARER_TOKEN` style. |
|
|
214
|
+
|
|
215
|
+
### Hand-rolled resolvers — `defineConnectionResolver`
|
|
216
|
+
|
|
217
|
+
Use the typed-identity helper to preserve the literal `name` (and `keySuffixes`) so consumer-side type narrowing on `RunOptions.connection` works:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
// Single-credential.
|
|
221
|
+
export const apiKeyResolver = defineConnectionResolver({
|
|
222
|
+
name: "api-key",
|
|
223
|
+
resolve:
|
|
224
|
+
(key) =>
|
|
225
|
+
(input, init = {}) =>
|
|
226
|
+
globalThis.fetch(input, {
|
|
227
|
+
...init,
|
|
228
|
+
headers: { ...(init.headers ?? {}), "X-API-Key": key },
|
|
229
|
+
}),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Multi-credential — `keySuffixes` declares which credentials the resolver needs.
|
|
233
|
+
// `resolve` receives a suffix-keyed bag (NOT name-prefixed); the wrapper strips
|
|
234
|
+
// the `<NAME>_` prefix for you.
|
|
235
|
+
export const hmacResolver = defineConnectionResolver({
|
|
236
|
+
name: "hmac",
|
|
237
|
+
keySuffixes: ["KEY", "SECRET"] as const,
|
|
238
|
+
resolve: ({ KEY, SECRET }) =>
|
|
239
|
+
/* ... build authed fetch ... */ globalThis.fetch,
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Env-only credentials
|
|
244
|
+
|
|
245
|
+
**The CLI never accepts secrets via argv.** Per-script (`handleIfScriptMain`) and connector-bin (`runDispatchCli`) entries take only the input JSON (positional arg or stdin) plus `--help`. Credentials come from environment variables.
|
|
246
|
+
|
|
247
|
+
### Env-var composition rules
|
|
248
|
+
|
|
249
|
+
The wrapper composes env-var names from `[<SLOT>_]<CONNECTION_KEY>_<RESOLVER_NAME>[_<SUFFIX>]`:
|
|
250
|
+
|
|
251
|
+
| Slot prefix | Connection key | Resolver name | Suffix | Composed env var |
|
|
252
|
+
| ---------------------- | -------------- | ---------------------- | ---------------- | -------------------------------------------- |
|
|
253
|
+
| (none — single-slot) | `notion` | `zapier-connection-id` | (none) | `NOTION_ZAPIER_CONNECTION_ID` |
|
|
254
|
+
| (none — single-slot) | `notion` | `token` | (none) | `NOTION_TOKEN` |
|
|
255
|
+
| `SOURCE_` (multi-slot) | `notion` | `token` | (none) | `SOURCE_NOTION_TOKEN` |
|
|
256
|
+
| `TARGET_` (multi-slot) | `notion` | `zapier-connection-id` | (none) | `TARGET_NOTION_ZAPIER_CONNECTION_ID` |
|
|
257
|
+
| (none — single-slot) | `some-api` | `hmac` | `KEY` / `SECRET` | `SOME_API_HMAC_KEY` + `SOME_API_HMAC_SECRET` |
|
|
258
|
+
|
|
259
|
+
Run a per-script CLI with `--help` to print the exact env vars each registered resolver demands for that script.
|
|
260
|
+
|
|
261
|
+
### CLI examples
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
# Single-conn — natural ecosystem-shape envs:
|
|
265
|
+
echo '{"query":"Q4 planning"}' \
|
|
266
|
+
| NOTION_TOKEN=secret_xxx node apps/notion/scripts/search.ts
|
|
267
|
+
|
|
268
|
+
# Single-conn — Zapier-relayed (the first resolver in `[zapier, bearer]` wins
|
|
269
|
+
# because `NOTION_ZAPIER_CONNECTION_ID` is the env var that turned up):
|
|
270
|
+
NOTION_ZAPIER_CONNECTION_ID=conn_xxx node apps/notion/scripts/search.ts \
|
|
271
|
+
'{"query":"Q4 planning"}'
|
|
272
|
+
|
|
273
|
+
# Multi-conn — per-slot env prefixes route each credential to the right slot:
|
|
274
|
+
SOURCE_NOTION_TOKEN=src_xxx \
|
|
275
|
+
TARGET_NOTION_TOKEN=tgt_xxx \
|
|
276
|
+
node apps/notion/scripts/copy-page.ts \
|
|
277
|
+
'{"sourcePageId":"...","targetParentPageId":"..."}'
|
|
278
|
+
|
|
279
|
+
# Per-script help — lists positional input shape + every resolver's env vars:
|
|
280
|
+
node apps/notion/scripts/search.ts --help
|
|
281
|
+
|
|
282
|
+
# Connector bin help — lists scripts; `<bin> <script> --help` for per-script env:
|
|
283
|
+
npx @zapier/notion-connector --help
|
|
284
|
+
npx @zapier/notion-connector search --help
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## In-process consumption — `script.run(input, opts)`
|
|
288
|
+
|
|
289
|
+
`ToolDefinition` carries the wrapped `.run`; consumers always pass an explicit `RunOptions`.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import notion, { search } from "@zapier/notion-connector";
|
|
293
|
+
|
|
294
|
+
// Bundle path:
|
|
295
|
+
await notion.scripts.search.run(
|
|
296
|
+
{ query: "Q4 planning" },
|
|
297
|
+
{ connection: { TOKEN: "secret_xxx" } },
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Named export (each export is the wrapped `.run`):
|
|
301
|
+
await search({ query: "Q4 planning" }, { connection: { TOKEN: "secret_xxx" } });
|
|
302
|
+
|
|
303
|
+
// Long-running consumers — build opts once, pass on every call:
|
|
304
|
+
const runOpts = notion.buildRunOptionsFromEnv(
|
|
305
|
+
notion.scripts.search,
|
|
306
|
+
process.env,
|
|
307
|
+
);
|
|
308
|
+
await notion.scripts.search.run({ query: "a" }, runOpts);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### `RunOptions.connection` shapes
|
|
312
|
+
|
|
313
|
+
A slot's `connection` (or per-slot entry under `connections`) accepts one of three shapes:
|
|
314
|
+
|
|
315
|
+
| Shape | Resolution |
|
|
316
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
317
|
+
| **`function`** (a `Fetch`) | Used as-is — no resolver consulted. Mocked-Fetch test paths use this. |
|
|
318
|
+
| **`string`** (shorthand) | Forwarded to the **first** `SingleConnectionResolver` in the slot's array. Throws when there isn't one (e.g. all resolvers in the array are multi-credential). |
|
|
319
|
+
| **`Record<string, string>`** | The keys are name-prefixed (`<NAME>` for single-credential, `<NAME>_<SUFFIX>` for multi). The wrapper picks the resolver whose `bagKeysFor` set matches `Object.keys(value)` exactly. |
|
|
320
|
+
|
|
321
|
+
The record bag is the canonical programmatic shape. `string` shorthand is a convenience when the slot has exactly one viable resolver and you don't want to spell its name out. Errors include the script name, slot label, and the available resolvers' shapes so multi-conn misconfig points the operator at the right credential.
|
|
322
|
+
|
|
323
|
+
### `RunOptions.connection` type narrowing
|
|
324
|
+
|
|
325
|
+
The wrapped `.run` narrows `RunOptions.connection` against the resolver entry registered for the slot's connection key. For Notion (`[zapierConnectionResolver, defineBearerTokenResolver()]`):
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
// All four are statically valid for `connection: "notion"`:
|
|
329
|
+
await notion.scripts.search.run(input, { connection: globalThis.fetch }); // (1) Fetch
|
|
330
|
+
await notion.scripts.search.run(input, { connection: "conn_xxx" }); // (2) string → first single-cred (zapier)
|
|
331
|
+
await notion.scripts.search.run(input, {
|
|
332
|
+
connection: { ZAPIER_CONNECTION_ID: "conn_xxx" },
|
|
333
|
+
}); // (3) zapier-prefixed bag
|
|
334
|
+
await notion.scripts.search.run(input, { connection: { TOKEN: "secret_xxx" } }); // (3) bearer-prefixed bag
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
A multi-credential resolver `{ name: "hmac", keySuffixes: ["KEY", "SECRET"] }` would contribute the bag shape `{ HMAC_KEY: string; HMAC_SECRET: string }` — the resolver's `name` always heads each bag key.
|
|
338
|
+
|
|
339
|
+
## `Context` — the author surface
|
|
340
|
+
|
|
341
|
+
| Slot cardinality | Author writes | `ctx` shape |
|
|
342
|
+
| ------------------------------ | ---------------------- | ----------------------------------------------------------------- |
|
|
343
|
+
| Credential-free | `run: (input) =>` | (no `ctx`) |
|
|
344
|
+
| Single (`connection: ...`) | `run: (input, ctx) =>` | `ctx.fetch` — the resolved authed `fetch` for the slot. |
|
|
345
|
+
| Multi (`connections: { ... }`) | `run: (input, ctx) =>` | `ctx.connections.<slot>` per declared slot. No `ctx.fetch` sugar. |
|
|
346
|
+
|
|
347
|
+
The author never sees `process.env`, never threads a `Fetch` through their script body, never re-implements auth logic.
|
|
348
|
+
|
|
349
|
+
## `inputDependencies` — out-of-band dependent-field metadata
|
|
350
|
+
|
|
351
|
+
Some scripts have input fields whose accepted shape depends on another input field — e.g. Notion's `create_database_item` takes a `databaseId` and a `properties` map whose schema is determined by which database was chosen. JSON Schema can't express that conditional shape, but adapters that drive option-loading or schema-resolution chains need to know about it.
|
|
352
|
+
|
|
353
|
+
`defineTool` publishes the metadata in two places:
|
|
354
|
+
|
|
355
|
+
1. `definition.inputDependencies` — programmatic readers reach for it directly on the script's default export.
|
|
356
|
+
2. MCP registration shape — `_meta["zapier:inputDependencies"]` via either `toMcpTool` (wire-format) or `toMcpServerTool` (`McpServer.registerTool` config).
|
|
357
|
+
|
|
358
|
+
Shape constraints:
|
|
359
|
+
|
|
360
|
+
- Dependencies are keyed by the dependent input field's name (`databaseId`, `properties`).
|
|
361
|
+
- Each entry names another tool by **string** (`fromTool: "list-databases"`), not by function reference — so the chain stays serializable.
|
|
362
|
+
- Args passed to the resolver tool use `$<fieldName>` syntax to reference other input fields (`fromArgs: { databaseId: "$databaseId" }`). The adapter reads and evaluates these references; the script body does not.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
inputDependencies: {
|
|
366
|
+
databaseId: { kind: "options", fromTool: "list-databases", fromArgs: {} },
|
|
367
|
+
properties: {
|
|
368
|
+
kind: "schema",
|
|
369
|
+
fromTool: "get-database-schema",
|
|
370
|
+
fromArgs: { databaseId: "$databaseId" },
|
|
371
|
+
},
|
|
372
|
+
} as const,
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## `<bin> mcp` — local-MCP-server execution surface
|
|
376
|
+
|
|
377
|
+
Reached through the bundled CLI as `npx @zapier/<x>-connector mcp` — no per-app glue. The dispatcher reserves the primary command `mcp` and delegates to the internal `serveMcpStdio` helper:
|
|
378
|
+
|
|
379
|
+
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.
|
|
380
|
+
2. Resolves server identity (`name` / `version`) from the `package.json` adjacent to the connector's `cli.ts` (via `meta.url`).
|
|
381
|
+
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.
|
|
382
|
+
4. Connects via `StdioServerTransport` and writes a one-line ready banner to stderr.
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
NOTION_TOKEN=secret_xxx npx @zapier/notion-connector mcp
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
For consumers who want to register connector scripts inside their **own** MCP server, there are two lower-level helpers depending on the SDK API used:
|
|
389
|
+
|
|
390
|
+
- Modern `McpServer.registerTool`: `toMcpServerTool(script)` + `connector.buildRunOptionsFromEnv` — see [`examples/mcp-server-tool/`](../../examples/mcp-server-tool/).
|
|
391
|
+
- Deprecated `Server.setRequestHandler`: `toMcpTool(script)` + `connector.buildRunOptionsFromEnv` — see [`examples/mcp-tool/`](../../examples/mcp-tool/).
|
|
392
|
+
|
|
393
|
+
## Why is the package surface this small?
|
|
394
|
+
|
|
395
|
+
`defineTool` + `defineConnector` + `handleIfScriptMain` + `runDispatchCli` + `toFunctions` is the authoring + execution contract every script and connector entry imports. SDK-shipped resolvers (`zapierConnectionResolver`, `defineBearerTokenResolver`) and the typed-identity `defineConnectionResolver` cover the most common auth paths; everything else flows through the same `ConnectionResolver` shape. Registration surface helpers and env partitioning live on the connector object, not the package entry, so MCP/OpenAI/CLI consumers depend on the connector bundle — not duplicate SDK exports.
|
|
396
|
+
|
|
397
|
+
The Zapier-Relay path (`zapierConnectionResolver`) lazy-imports `@zapier/zapier-sdk` only when it actually wins resolution — connectors that don't expose Zapier auth don't pull the SDK into their dependency closure.
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/build-zapier-fetch.ts
|
|
2
|
+
async function buildZapierFetch(connectionId) {
|
|
3
|
+
if (!connectionId) {
|
|
4
|
+
throw new Error(
|
|
5
|
+
"buildZapierFetch: connectionId is required (the Zapier connection UUID)."
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
let sdkModule;
|
|
9
|
+
try {
|
|
10
|
+
sdkModule = await import("@zapier/zapier-sdk");
|
|
11
|
+
} catch (err) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"buildZapierFetch: `@zapier/zapier-sdk` is not installed. Zapier mode requires the SDK as a peer dependency \u2014 list it alongside `@zapier/connectors-sdk` in the connector's `package.json`, and run `zapier-sdk login` (or set `ZAPIER_TOKEN`) on the host before invoking the script.",
|
|
14
|
+
{ cause: err }
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const sdk = sdkModule.createZapierSdk();
|
|
18
|
+
return (async (input, init = {}) => {
|
|
19
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
20
|
+
const method = init.method ?? "GET";
|
|
21
|
+
const headers = normaliseHeaders(init.headers);
|
|
22
|
+
const body = init.body;
|
|
23
|
+
if (body != null && typeof body !== "string") {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"buildZapierFetch: Zapier-mode `fetch` only accepts `body: string | undefined`. Serialize the body (e.g. `JSON.stringify(...)`) before calling. Streaming bodies, `FormData`, `Blob`, and `ArrayBuffer` are not supported in Zapier mode."
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const response = await sdk.fetch(url, {
|
|
29
|
+
method,
|
|
30
|
+
authenticationId: connectionId,
|
|
31
|
+
headers,
|
|
32
|
+
body: body ?? void 0
|
|
33
|
+
});
|
|
34
|
+
return response;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function normaliseHeaders(h) {
|
|
38
|
+
if (!h) return void 0;
|
|
39
|
+
if (h instanceof Headers) return Object.fromEntries(h.entries());
|
|
40
|
+
if (Array.isArray(h)) {
|
|
41
|
+
return Object.fromEntries(h.map(([k, v]) => [k, flattenValue(v)]));
|
|
42
|
+
}
|
|
43
|
+
return Object.fromEntries(
|
|
44
|
+
Object.entries(h).map(([k, v]) => [k, flattenValue(v)])
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
function flattenValue(v) {
|
|
48
|
+
return Array.isArray(v) ? v.join(", ") : v;
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
buildZapierFetch
|
|
52
|
+
};
|