@getsales/outreach-mcp 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -2
  3. package/bin/cli.js +187 -0
  4. package/package.json +37 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GetSales
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,85 @@
1
- # Temporary Holding Version
1
+ # GetSales: Outreach MCP Server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@getsales/outreach-mcp` connects an MCP client that can only start a command (Claude Desktop, Cursor, an agent runtime) to the hosted GetSales server at `https://mcp2.getsales.io/mcp/public`. It starts [mcp-remote](https://github.com/punkpeye/mcp-remote) and nothing else: the tools run on the server, which keeps each call inside your workspace.
4
+
5
+ GetSales is LinkedIn and email outreach for agencies and GTM teams, in one app. Describe who you sell to, and GetSales finds the people and their work emails, scores them and writes the first lines, and every reply comes back to a team inbox.
6
+
7
+ Through the server an AI agent works with the tables of people and companies, the enrichment columns, the workflows, the inbox with the AI SDR's drafts, the tasks, the website signals and the webhooks of a GetSales workspace. It sees four meta-tools: `search_tools` finds a tool by what it does, `describe_tool` returns the definition, `call_tool` runs the tool, and `list_toolsets` maps the catalog. A tool the server marks dangerous, such as a delete, a cancel or a subscription change, answers a preview and a `commit_token` first, and runs only when the same call comes back with that token as the confirm.
8
+
9
+ ## Start it from your client
10
+
11
+ Claude Desktop, in `claude_desktop_config.json`:
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "getsales": {
17
+ "command": "npx",
18
+ "args": ["-y", "@getsales/outreach-mcp"]
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ Claude Code:
25
+
26
+ ```
27
+ claude mcp add getsales -- npx -y @getsales/outreach-mcp
28
+ ```
29
+
30
+ The first start opens the GetSales consent page in a browser. With no account yet, [sign up](https://getsales.io/signup/) before that step or from the page itself. A client that speaks streamable HTTP needs no launcher: give it `https://mcp2.getsales.io/mcp/public`, and it runs the same sign-in on its own.
31
+
32
+ ## Sign in with OAuth or with a key
33
+
34
+ The launcher picks one way per start and never mixes them.
35
+
36
+ - **OAuth, when `GETSALES_API_KEY` is not set.** mcp-remote gets a 401 with a `WWW-Authenticate` challenge, reads the authorization server from the protected-resource document (`https://mcp2.getsales.io/.well-known/oauth-protected-resource`), registers itself there and runs the consent in your browser with PKCE. The tokens stay in `~/.mcp-auth`: treat that folder like a password store.
37
+ - **A key, when `GETSALES_API_KEY` is set.** Make the key in the app under Settings, API keys. The launcher tests it with one `initialize` request, then hands mcp-remote a header that names the variable, `Authorization: Bearer ${GETSALES_API_KEY}`, and mcp-remote fills the value in from its environment. So the key sits on no command line and in no log line, and no browser opens. A key the server turns down (401: wrong, revoked or expired) or a workspace whose subscription is on hold (402) stops the launcher at once, with the reason on stderr. When the test cannot reach the server, the launcher says so and starts anyway.
38
+
39
+ Why never both: a fixed bearer and OAuth on one connection work against each other. mcp-remote would answer a refused key by starting OAuth, and the fixed header would then replace each token that flow got.
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "getsales": {
45
+ "command": "npx",
46
+ "args": ["-y", "@getsales/outreach-mcp"],
47
+ "env": { "GETSALES_API_KEY": "gs_live_..." }
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ ## Settings
54
+
55
+ - `GETSALES_API_KEY`: the key for the key mode. Leave it unset for OAuth, except in the Docker image, which needs it.
56
+ - `GETSALES_MCP_URL`: another endpoint. The default is `https://mcp2.getsales.io/mcp/public`.
57
+ - Arguments after the command go to mcp-remote: `--debug` writes a detailed log to `~/.mcp-auth`, `--host` sets the callback hostname, and `--allow-http` accepts a plain-http endpoint.
58
+
59
+ ## The Docker image
60
+
61
+ `getsalesio/outreach-mcp` runs this launcher in a container, for a host without Node.js, and signs in with a key only. Pass `-e` with the bare variable name and docker copies the value from the client's `env` block, so the key stays off the docker command line:
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "getsales": {
67
+ "command": "docker",
68
+ "args": ["run", "-i", "--rm", "-e", "GETSALES_API_KEY", "getsalesio/outreach-mcp"],
69
+ "env": { "GETSALES_API_KEY": "gs_live_..." }
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ From a shell, export the key first, then run `docker run -i --rm -e GETSALES_API_KEY getsalesio/outreach-mcp`.
76
+
77
+ OAuth cannot finish in a container. After the consent page your browser comes back to `localhost`, which is your machine, while mcp-remote waits for that answer on the container's own loopback. So without a key the container stops at once and says why. For OAuth, run the launcher with npx or give the client the URL.
78
+
79
+ ## Where to read more
80
+
81
+ - Keys and OAuth: [docs.getsales.io/authentication](https://docs.getsales.io/authentication)
82
+ - The API reference: [docs.getsales.io](https://docs.getsales.io)
83
+ - The product: [getsales.io/ai-sales-assistant](https://getsales.io/ai-sales-assistant/)
84
+
85
+ GetSales is an independent product and is not affiliated with, endorsed by, or sponsored by LinkedIn Corporation. LinkedIn and the LinkedIn logo are trademarks of LinkedIn Corporation. Use of GetSales is at your own risk and subject to the applicable third-party platform terms.
package/bin/cli.js ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // getsales-outreach-mcp
5
+ //
6
+ // For an MCP client that can only start a command: it reaches the hosted
7
+ // GetSales workspace server through mcp-remote, the one dependency pinned in
8
+ // package.json. The tools run on the server, never here. The image built from
9
+ // the repository's Dockerfile starts this same file.
10
+ //
11
+ // How it signs in:
12
+ //
13
+ // GETSALES_API_KEY unset mcp-remote does OAuth. It follows the server's
14
+ // 401 challenge to the GetSales consent page and
15
+ // keeps the tokens in ~/.mcp-auth.
16
+ // GETSALES_API_KEY set the key is the bearer, and no browser opens. One
17
+ // initialize request tests the key first, so a dead
18
+ // key stops here with a plain line on stderr. Left
19
+ // to mcp-remote, a refused bearer would start the
20
+ // OAuth flow, and the fixed header would then
21
+ // replace every token that flow obtained.
22
+ //
23
+ // Where the key travels: into the test request this process sends, and into
24
+ // the environment of mcp-remote. The command line of mcp-remote carries the
25
+ // literal text ${GETSALES_API_KEY}, which mcp-remote fills in from its own
26
+ // environment after it has logged its arguments. A process list and the
27
+ // client's log of this server's stderr show the variable's name, never its
28
+ // value. Nothing this file prints quotes the key, or the text of an error that
29
+ // could quote it.
30
+ //
31
+ // Test seam: with GETSALES_MCP_DRY_RUN=1 the file prints what it would start,
32
+ // as one JSON line on stdout, after the key test and instead of the bridge.
33
+ // tests/launcher-kit.test.mjs in the repository drives it.
34
+
35
+ const { spawn } = require('node:child_process');
36
+ const path = require('node:path');
37
+ const manifest = require('../package.json');
38
+
39
+ const DEFAULT_ENDPOINT = 'https://mcp2.getsales.io/mcp/public';
40
+ const KEY_HEADER = 'Authorization: Bearer ${GETSALES_API_KEY}';
41
+ const RELAYED = ['SIGINT', 'SIGTERM', 'SIGHUP'];
42
+ const CHECK_SECONDS = 10;
43
+
44
+ const REFUSED = {
45
+ 401: [
46
+ 'the server turned GETSALES_API_KEY down (HTTP 401): the key is wrong, revoked or past its expiry date.',
47
+ 'Make a new key in the app under Settings, API keys, or remove GETSALES_API_KEY to sign in through the browser.',
48
+ ],
49
+ 402: [
50
+ 'GETSALES_API_KEY is a working key, but the subscription of its workspace is on hold (HTTP 402).',
51
+ 'Bring the subscription back in the app, then start this again.',
52
+ ],
53
+ };
54
+
55
+ const binName = Object.keys(manifest.bin)[0];
56
+ const say = (line) => process.stderr.write(`${binName}: ${line}\n`);
57
+
58
+ // A short reason for a failure, built only from fixed names and codes: the
59
+ // message of a network error can repeat the request it failed on.
60
+ function reasonOf(error) {
61
+ if (!error) return 'unknown error';
62
+ if (error.name === 'TimeoutError' || error.name === 'AbortError') return `no answer within ${CHECK_SECONDS} seconds`;
63
+ for (const code of [error.code, error.cause && error.cause.code]) {
64
+ if (typeof code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(code)) return code;
65
+ }
66
+ return typeof error.name === 'string' ? error.name : 'unknown error';
67
+ }
68
+
69
+ function isUrl(text) {
70
+ try {
71
+ new URL(text);
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ function initializeBody() {
79
+ return JSON.stringify({
80
+ jsonrpc: '2.0',
81
+ id: 'key-check',
82
+ method: 'initialize',
83
+ params: {
84
+ protocolVersion: '2025-03-26',
85
+ capabilities: {},
86
+ clientInfo: { name: manifest.name, version: manifest.version },
87
+ },
88
+ });
89
+ }
90
+
91
+ // true: start the bridge. false: the server refused the key, and the reason
92
+ // is on stderr already.
93
+ async function keyAccepted(endpoint, key) {
94
+ let response;
95
+ try {
96
+ response = await fetch(endpoint, {
97
+ method: 'POST',
98
+ headers: {
99
+ authorization: `Bearer ${key}`,
100
+ 'content-type': 'application/json',
101
+ accept: 'application/json, text/event-stream',
102
+ },
103
+ body: initializeBody(),
104
+ signal: AbortSignal.timeout(CHECK_SECONDS * 1000),
105
+ });
106
+ } catch (error) {
107
+ say(`could not test GETSALES_API_KEY on ${new URL(endpoint).host} (${reasonOf(error)}). Starting anyway: the first call will tell.`);
108
+ return true;
109
+ }
110
+ // Only the status counts. The body is dropped unread, so an answer that
111
+ // streams cannot hold the start back.
112
+ if (response.body) await response.body.cancel().catch(() => undefined);
113
+ const lines = REFUSED[response.status];
114
+ if (lines) lines.forEach(say);
115
+ return !lines;
116
+ }
117
+
118
+ // The file that mcp-remote's package.json names as its command.
119
+ function bridgeEntry() {
120
+ const packageFile = require.resolve('mcp-remote/package.json');
121
+ return path.join(path.dirname(packageFile), require(packageFile).bin['mcp-remote']);
122
+ }
123
+
124
+ function startBridge(args, env) {
125
+ const bridge = spawn(process.execPath, args, { stdio: 'inherit', env });
126
+ const relay = (signal) => bridge.kill(signal);
127
+ RELAYED.forEach((signal) => process.on(signal, relay));
128
+ bridge.once('error', (error) => {
129
+ say(`mcp-remote did not start (${reasonOf(error)}).`);
130
+ process.exitCode = 1;
131
+ });
132
+ bridge.once('exit', (code, signal) => {
133
+ RELAYED.forEach((name) => process.removeListener(name, relay));
134
+ if (signal) process.kill(process.pid, signal);
135
+ else process.exitCode = code === null ? 0 : code;
136
+ });
137
+ }
138
+
139
+ async function main() {
140
+ const endpoint = process.env.GETSALES_MCP_URL || DEFAULT_ENDPOINT;
141
+ if (!isUrl(endpoint)) {
142
+ say('GETSALES_MCP_URL is not a URL.');
143
+ return 1;
144
+ }
145
+
146
+ const env = { ...process.env };
147
+ const key = (env.GETSALES_API_KEY || '').trim();
148
+ if (key) {
149
+ if (!/^[\x21-\x7e]+$/.test(key)) {
150
+ say('GETSALES_API_KEY holds a space, a line break or another character a header cannot carry. Copy the key again from the app (Settings, API keys).');
151
+ return 1;
152
+ }
153
+ env.GETSALES_API_KEY = key;
154
+ if (!(await keyAccepted(endpoint, key))) return 1;
155
+ } else {
156
+ delete env.GETSALES_API_KEY;
157
+ }
158
+
159
+ let entry;
160
+ try {
161
+ entry = bridgeEntry();
162
+ } catch (error) {
163
+ say(`the mcp-remote package is missing (${reasonOf(error)}). Install ${manifest.name} again.`);
164
+ return 1;
165
+ }
166
+
167
+ const args = [entry, endpoint, '--transport', 'http-only', ...(key ? ['--header', KEY_HEADER] : []), ...process.argv.slice(2)];
168
+ if (process.env.GETSALES_MCP_DRY_RUN === '1') {
169
+ process.stdout.write(`${JSON.stringify({ command: process.execPath, args })}\n`);
170
+ return 0;
171
+ }
172
+ startBridge(args, env);
173
+ return undefined;
174
+ }
175
+
176
+ // An exit code is set, never process.exit(), so the lines written to a pipe
177
+ // above reach it whole before the process ends. An unexpected failure is
178
+ // reported by its name alone, for the reason reasonOf gives.
179
+ main().then(
180
+ (code) => {
181
+ if (code !== undefined) process.exitCode = code;
182
+ },
183
+ (error) => {
184
+ say(`stopped (${reasonOf(error)}).`);
185
+ process.exitCode = 1;
186
+ },
187
+ );
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@getsales/outreach-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Connects a stdio MCP client to the hosted GetSales outreach MCP server. An AI agent works a GetSales workspace through it: tables of people and companies, enrichment, workflows, the inbox, tasks and signals, behind four MCP tools, and the tools the server marks dangerous preview before they run.",
5
+ "author": "GetSales (https://getsales.io/)",
6
+ "mcpName": "io.getsales/outreach-mcp",
7
+ "bin": {
8
+ "getsales-outreach-mcp": "bin/cli.js"
9
+ },
10
+ "files": [
11
+ "bin"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/getsales-io/outreach-mcp.git",
20
+ "directory": "npm"
21
+ },
22
+ "homepage": "https://getsales.io/ai-sales-assistant/",
23
+ "bugs": {
24
+ "url": "https://github.com/getsales-io/outreach-mcp/issues"
25
+ },
26
+ "keywords": [
27
+ "outreach",
28
+ "cold-email",
29
+ "sales-engagement",
30
+ "ai-sdr",
31
+ "enrichment",
32
+ "mcp",
33
+ "mcp-server",
34
+ "model-context-protocol"
35
+ ],
36
+ "dependencies": {
37
+ "mcp-remote": "0.1.38"
38
+ }
39
+ }