@testomatio/mcp 2.2.1 → 3.0.0-beta.2

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.md CHANGED
@@ -47,7 +47,15 @@ export TESTOMATIO_PROJECT_ID=<PROJECT_ID>
47
47
  testomatio-mcp
48
48
  ```
49
49
 
50
- **Optional: custom base URL**
50
+ **Optional: custom host**
51
+ ```bash
52
+ export TESTOMATIO_HOST=beta.testomat.io
53
+ testomatio-mcp --host beta.testomat.io
54
+ ```
55
+
56
+ A bare hostname is expanded to `https://<host>`. For full control use
57
+ `--base-url` / `TESTOMATIO_BASE_URL`, which takes precedence over the host option:
58
+
51
59
  ```bash
52
60
  export TESTOMATIO_BASE_URL=https://beta.testomat.io
53
61
  ```
@@ -151,6 +159,48 @@ Add this config to `opencode.json` in your project root, or to `~/.config/openco
151
159
  }
152
160
  ```
153
161
 
162
+ ## HTTP Transport
163
+
164
+ Besides stdio, the server runs over Streamable HTTP on a Cloudflare Worker hosted by
165
+ Testomat.io. The project is part of the URL, so every tool signature stays the same:
166
+
167
+ ```
168
+ https://mcp.testomat.io/mcp/<project_id>
169
+ ```
170
+
171
+ Point an MCP client at that URL with a project token:
172
+
173
+ ```json
174
+ {
175
+ "mcpServers": {
176
+ "testomatio": {
177
+ "url": "https://mcp.testomat.io/mcp/<PROJECT_ID>",
178
+ "headers": {
179
+ "Authorization": "Bearer <PROJECT_TOKEN>"
180
+ }
181
+ }
182
+ }
183
+ }
184
+ ```
185
+
186
+ Web connectors such as claude.ai have nowhere to put a static token and instead run
187
+ OAuth 2.1 with PKCE and Dynamic Client Registration against the same URL. Tokens
188
+ starting with `testomat_` or `tstmt_` always bypass OAuth and are passed straight
189
+ through, so IDE clients and CI keep working.
190
+
191
+ The endpoint is POST only; `GET` returns `405`, because the server never initiates
192
+ traffic. Testing with `curl` requires both media types in `Accept`:
193
+
194
+ ```bash
195
+ curl -sS https://mcp.testomat.io/mcp/<PROJECT_ID> \
196
+ -H "Authorization: Bearer <PROJECT_TOKEN>" \
197
+ -H "Content-Type: application/json" \
198
+ -H "Accept: application/json, text/event-stream" \
199
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
200
+ ```
201
+
202
+ Self-hosted installations keep using stdio.
203
+
154
204
  ## Quick Examples
155
205
 
156
206
  **List tests:**
@@ -232,7 +282,8 @@ src/
232
282
  | `TESTOMATIO_PROJECT_TOKEN` | Yes* | - | Project token (preferred) |
233
283
  | `TESTOMATIO_API_TOKEN` | Yes* | - | Alternative token |
234
284
  | `TESTOMATIO_PROJECT_ID` | Yes | - | Project ID |
235
- | `TESTOMATIO_BASE_URL` | No | `https://app.testomat.io` | API base URL |
285
+ | `TESTOMATIO_HOST` | No | - | API host, e.g. `beta.testomat.io` |
286
+ | `TESTOMATIO_BASE_URL` | No | `https://app.testomat.io` | API base URL, wins over `TESTOMATIO_HOST` |
236
287
  | `TESTOMATIO_TOOLS` | No | `full` | Tool profile: `full`, `core`, or `read` |
237
288
 
238
289
  *Either `TESTOMATIO_PROJECT_TOKEN` or `TESTOMATIO_API_TOKEN`
@@ -287,6 +338,7 @@ NODE_EXTRA_CA_CERTS=/path/to/company-root-ca.pem testomatio-mcp --token <TOKEN>
287
338
  ```bash
288
339
  npm install
289
340
  npm run start -- --token <TOKEN> --project <PROJECT_ID>
341
+ npm test
290
342
  ```
291
343
 
292
344
  For local MCP development, point Claude Desktop to the checked-out entrypoint:
@@ -372,3 +424,37 @@ Example `analytics_charts_results` call:
372
424
  }
373
425
  }
374
426
  ```
427
+
428
+ ### Worker deployment
429
+
430
+ The `worker/` directory holds the Cloudflare Worker and is excluded from the npm
431
+ package. Deploy it from that directory:
432
+
433
+ ```bash
434
+ cd worker
435
+ npx wrangler kv namespace create OAUTH_KV
436
+ npx wrangler secret put TESTOMATIO_MCP_WORKER_SECRET
437
+ npx wrangler deploy
438
+ ```
439
+
440
+ Put the namespace id returned by the first command into `kv_namespaces` in
441
+ `worker/wrangler.jsonc`. `TESTOMATIO_MCP_WORKER_SECRET` is the shared secret used to
442
+ redeem authorization codes against Testomat.io server-to-server and is never
443
+ committed.
444
+
445
+ Before exposing the Worker publicly, configure Cloudflare rate-limiting rules for
446
+ `/register`, `/authorize`, `/token`, and `/mcp/*`. These endpoints intentionally
447
+ support unauthenticated OAuth discovery and client registration, so rate limiting
448
+ belongs at the edge rather than in per-isolate memory. Keep separate rules and KV
449
+ namespaces for beta and production.
450
+
451
+ A beta worker is the same code deployed to the `beta` environment, which targets
452
+ `https://beta.testomat.io` and keeps its own KV namespace so beta grants never
453
+ reach the production one:
454
+
455
+ ```bash
456
+ npx wrangler kv namespace create OAUTH_KV --env beta
457
+ npx wrangler secret put TESTOMATIO_MCP_WORKER_SECRET --env beta
458
+ npx wrangler deploy --env beta
459
+ ```
460
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/mcp",
3
- "version": "2.2.1",
3
+ "version": "3.0.0-beta.2",
4
4
  "description": "Model Context Protocol server for Testomatio API",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -10,7 +10,8 @@
10
10
  "scripts": {
11
11
  "start": "node index.js",
12
12
  "dev": "node index.js",
13
- "sync:enterprise-package": "node scripts/sync-enterprise-package.js"
13
+ "sync:enterprise-package": "node scripts/sync-enterprise-package.js",
14
+ "test": "vitest run"
14
15
  },
15
16
  "keywords": [
16
17
  "testomatio",
@@ -27,7 +28,7 @@
27
28
  "url": "https://github.com/testomatio/mcp"
28
29
  },
29
30
  "dependencies": {
30
- "@modelcontextprotocol/sdk": "^0.4.0",
31
+ "@modelcontextprotocol/sdk": "^1.30.0",
31
32
  "commander": "^12.0.0",
32
33
  "dotenv": "^17.2.3"
33
34
  },
@@ -39,5 +40,11 @@
39
40
  "engines": {
40
41
  "node": ">=18.0.0"
41
42
  },
42
- "devDependencies": {}
43
+ "devDependencies": {
44
+ "@cfworker/json-schema": "^4.1.1",
45
+ "@cloudflare/vitest-pool-workers": "^0.22.0",
46
+ "@cloudflare/workers-oauth-provider": "^0.10.3",
47
+ "vitest": "^4.1.11",
48
+ "wrangler": "^4.131.2"
49
+ }
43
50
  }
@@ -1,4 +1,5 @@
1
1
  import { HttpClient } from './http-client.js';
2
+ import { encodePathParameter } from '../core/path-segment.js';
2
3
 
3
4
  export class TestomatioApiClient {
4
5
  constructor({ baseUrl, projectId, token, logger }) {
@@ -14,9 +15,12 @@ export class TestomatioApiClient {
14
15
  }
15
16
 
16
17
  buildPath(resource, id = '') {
18
+ const safeProjectId = encodePathParameter(this.projectId, 'Project ID');
17
19
  const safeResource = String(resource).replace(/^\/+|\/+$/g, '');
18
- const safeId = id ? `/${String(id).replace(/^\/+|\/+$/g, '')}` : '';
19
- return `/api/v2/${this.projectId}/${safeResource}${safeId}`;
20
+ const resourceId = String(id ?? '');
21
+ const safeId = resourceId ? `/${encodePathParameter(resourceId, 'Resource ID')}` : '';
22
+
23
+ return `/api/v2/${safeProjectId}/${safeResource}${safeId}`;
20
24
  }
21
25
 
22
26
  list(resource, query = {}) {
package/src/cli/main.js CHANGED
@@ -15,6 +15,7 @@ export function parseArgs(argv = process.argv) {
15
15
  .option('-t, --token <token>', 'Testomatio Project token')
16
16
  .option('-p, --project <project>', 'Project ID')
17
17
  .option('--base-url <url>', 'Base URL for Testomatio API')
18
+ .option('--host <host>', 'Testomatio host, e.g. beta.testomat.io')
18
19
  .option(
19
20
  '--tools <profile>',
20
21
  'Tool surface: full (default, all tools), core (common entities only), read (read-only)'
@@ -10,13 +10,43 @@ function normalizeBaseUrl(value) {
10
10
  return normalized.replace(/\/+$/, '');
11
11
  }
12
12
 
13
- export function loadConfig(argvOptions = {}) {
13
+ function hostToBaseUrl(value) {
14
+ const host = normalizeBaseUrl(value);
15
+
16
+ if (!host) {
17
+ return '';
18
+ }
19
+
20
+ return /^https?:\/\//i.test(host) ? host : `https://${host}`;
21
+ }
22
+
23
+ export function resolveBaseUrl(argvOptions = {}, env = process.env) {
24
+ const explicitBaseUrl = normalizeBaseUrl(argvOptions.baseUrl || env.TESTOMATIO_BASE_URL);
25
+ if (explicitBaseUrl) {
26
+ return explicitBaseUrl;
27
+ }
28
+
29
+ const hostBaseUrl = hostToBaseUrl(argvOptions.host || env.TESTOMATIO_HOST);
30
+ if (hostBaseUrl) {
31
+ return hostBaseUrl;
32
+ }
33
+
34
+ return DEFAULT_BASE_URL;
35
+ }
36
+
37
+ export function loadServerConfig(argvOptions = {}, env = process.env) {
38
+ return {
39
+ baseUrl: resolveBaseUrl(argvOptions, env),
40
+ };
41
+ }
42
+
43
+ export function loadConfig(argvOptions = {}, env = process.env) {
14
44
  const token = normalizeString(
15
- argvOptions.token || process.env.TESTOMATIO_PROJECT_TOKEN || process.env.TESTOMATIO_API_TOKEN
45
+ argvOptions.token || env.TESTOMATIO_PROJECT_TOKEN || env.TESTOMATIO_API_TOKEN
16
46
  );
17
- const projectId = normalizeString(argvOptions.project || process.env.TESTOMATIO_PROJECT_ID);
18
- const baseUrl = normalizeBaseUrl(argvOptions.baseUrl || process.env.TESTOMATIO_BASE_URL || DEFAULT_BASE_URL);
19
- const rawToolsProfile = normalizeString(argvOptions.tools || process.env.TESTOMATIO_TOOLS).toLowerCase();
47
+ const projectId = normalizeString(argvOptions.project || env.TESTOMATIO_PROJECT_ID);
48
+ const baseUrl = resolveBaseUrl(argvOptions, env);
49
+ const rawToolsProfile = normalizeString(argvOptions.tools || env.TESTOMATIO_TOOLS).toLowerCase();
20
50
  const toolsProfile = rawToolsProfile || DEFAULT_PROFILE;
21
51
 
22
52
  if (!token) {
@@ -0,0 +1,21 @@
1
+ function pathParameter(value, label = 'Path parameter') {
2
+ const segment = String(value ?? '');
3
+
4
+ if (!segment || segment === '.' || segment === '..' || segment.includes('/') || segment.includes('\\')) {
5
+ throw new TypeError(`${label} must be a single URL path segment`);
6
+ }
7
+
8
+ return segment;
9
+ }
10
+
11
+ export function encodePathParameter(value, label) {
12
+ return encodeURIComponent(pathParameter(value, label));
13
+ }
14
+
15
+ export function decodePathParameter(value, label) {
16
+ try {
17
+ return pathParameter(decodeURIComponent(value), label);
18
+ } catch {
19
+ return '';
20
+ }
21
+ }
@@ -0,0 +1,16 @@
1
+ import { TestomatioApiClient } from '../api/testomatio-client.js';
2
+ import { createLogger } from '../core/logger.js';
3
+ import { TestomatioMCPServer } from './server.js';
4
+
5
+ export function createMcpServer({ token, projectId, baseUrl, logger, version, jsonSchemaValidator }) {
6
+ const config = { token, projectId, baseUrl };
7
+ const serverLogger = logger || createLogger();
8
+
9
+ return new TestomatioMCPServer({
10
+ config,
11
+ apiClient: new TestomatioApiClient({ ...config, logger: serverLogger }),
12
+ logger: serverLogger,
13
+ version,
14
+ jsonSchemaValidator,
15
+ });
16
+ }
@@ -0,0 +1,110 @@
1
+ export const SHARES_TOOLS = [
2
+ {
3
+ "name": "tests_share",
4
+ "description": "Share tests into a suite of another project (/api/v2/{project_id}/shares/tests). Select tests by test_ids, labels, or both (combined). The source project stays the single source of truth; shared copies in the target project are read-only until unlinked. Re-sharing into the same target project does not duplicate. Requests are processed asynchronously: status 'queued' means accepted, not completed. Matched tests that are themselves shared copies are skipped and listed in skipped_test_ids. Source and target projects must be of the same type (Classic/BDD).",
5
+ "inputSchema": {
6
+ "type": "object",
7
+ "properties": {
8
+ "test_ids": {
9
+ "type": "array",
10
+ "items": {
11
+ "type": "string"
12
+ },
13
+ "description": "Test IDs to share. At least one of test_ids/labels is required. Max 1000 tests per request."
14
+ },
15
+ "labels": {
16
+ "type": "array",
17
+ "items": {
18
+ "type": "string"
19
+ },
20
+ "description": "Label slugs or titles; every test carrying any of these labels is shared, in addition to test_ids."
21
+ },
22
+ "target_project_id": {
23
+ "type": "string",
24
+ "description": "Project ID (slug) of the destination project. Must be accessible to the current user."
25
+ },
26
+ "target_suite_id": {
27
+ "type": "string",
28
+ "description": "Suite ID in the target project to place the shared tests into. Must be a file-type suite, not a folder."
29
+ }
30
+ },
31
+ "required": [
32
+ "target_project_id",
33
+ "target_suite_id"
34
+ ],
35
+ "additionalProperties": false
36
+ }
37
+ },
38
+ {
39
+ "name": "suites_share",
40
+ "description": "Share suites (with their tests) into one or more other projects (/api/v2/{project_id}/shares/suites). Select suites by suite_ids, labels, or both (combined). File-type suites are linked (read-only copies that stay in sync); folder suites are deep-copied as regular editable copies. Re-sharing a linked suite into a project that already has it does not duplicate. Suites that are themselves shared copies link to their original source. Omit destination_folder_id to share into the root of the target project(s). Requests are processed asynchronously: status 'queued' means accepted, not completed. Source and target projects must be of the same type (Classic/BDD).",
41
+ "inputSchema": {
42
+ "type": "object",
43
+ "properties": {
44
+ "suite_ids": {
45
+ "type": "array",
46
+ "items": {
47
+ "type": "string"
48
+ },
49
+ "description": "Suite IDs to share. At least one of suite_ids/labels is required. Max 200 suites per request."
50
+ },
51
+ "labels": {
52
+ "type": "array",
53
+ "items": {
54
+ "type": "string"
55
+ },
56
+ "description": "Label slugs or titles; every suite carrying any of these labels is shared, in addition to suite_ids."
57
+ },
58
+ "target_project_ids": {
59
+ "type": "array",
60
+ "items": {
61
+ "type": "string"
62
+ },
63
+ "description": "Project IDs (slugs) of the destination projects. Must be accessible to the current user."
64
+ },
65
+ "destination_folder_id": {
66
+ "type": "string",
67
+ "description": "Folder suite ID in the target project to place the shared suites into. Only allowed when sharing to a single target project."
68
+ }
69
+ },
70
+ "required": [
71
+ "target_project_ids"
72
+ ],
73
+ "additionalProperties": false
74
+ }
75
+ },
76
+ {
77
+ "name": "tests_unshare",
78
+ "description": "Remove a test's share, converting the shared copy back into a regular, editable test (/api/v2/{project_id}/shares/tests/{id}). Only the shared copy can be targeted — the original source test is untouched. Must be called against the project that holds the shared copy.",
79
+ "inputSchema": {
80
+ "type": "object",
81
+ "properties": {
82
+ "test_id": {
83
+ "type": "string",
84
+ "description": "ID of the shared test copy to unlink."
85
+ }
86
+ },
87
+ "required": [
88
+ "test_id"
89
+ ],
90
+ "additionalProperties": false
91
+ }
92
+ },
93
+ {
94
+ "name": "suites_unshare",
95
+ "description": "Remove a suite's share, converting the shared copy back into a regular, editable suite (/api/v2/{project_id}/shares/suites/{id}). Only the shared (linked) copy can be targeted — the original source suite is untouched. Must be called against the project that holds the shared copy.",
96
+ "inputSchema": {
97
+ "type": "object",
98
+ "properties": {
99
+ "suite_id": {
100
+ "type": "string",
101
+ "description": "ID of the shared suite copy to unlink."
102
+ }
103
+ },
104
+ "required": [
105
+ "suite_id"
106
+ ],
107
+ "additionalProperties": false
108
+ }
109
+ }
110
+ ];
@@ -0,0 +1,53 @@
1
+ export const shareMethods = {
2
+ registerShareHandlers(handlers) {
3
+ handlers.tests_share = async (args = {}) => {
4
+ const payload = {
5
+ target_project_id: this.pickRequiredArg(args, 'target_project_id'),
6
+ target_suite_id: this.pickRequiredArg(args, 'target_suite_id'),
7
+ };
8
+ this.pickSelectionArgs(args, ['test_ids', 'labels'], payload);
9
+ return this.asText(await this.apiClient.create('shares/tests', payload));
10
+ };
11
+
12
+ handlers.suites_share = async (args = {}) => {
13
+ const payload = {
14
+ target_project_ids: this.pickRequiredArg(args, 'target_project_ids'),
15
+ };
16
+ this.pickSelectionArgs(args, ['suite_ids', 'labels'], payload);
17
+ if (args.destination_folder_id !== undefined) {
18
+ payload.destination_folder_id = args.destination_folder_id;
19
+ }
20
+ return this.asText(await this.apiClient.create('shares/suites', payload));
21
+ };
22
+
23
+ handlers.tests_unshare = async (args = {}) =>
24
+ this.asText(await this.apiClient.delete('shares/tests', this.pickRequiredArg(args, 'test_id')));
25
+
26
+ handlers.suites_unshare = async (args = {}) =>
27
+ this.asText(
28
+ await this.apiClient.delete('shares/suites', this.pickRequiredArg(args, 'suite_id'))
29
+ );
30
+ },
31
+
32
+ /**
33
+ * Copy the selection args (e.g. test_ids/labels) into the payload, requiring at least
34
+ * one of them — the API rejects a share request without a selection.
35
+ */
36
+ pickSelectionArgs(args = {}, keys, payload) {
37
+ const provided = keys.filter((key) => {
38
+ const value = args[key];
39
+ if (value === undefined || value === null || value === '') return false;
40
+ if (Array.isArray(value) && value.length === 0) return false;
41
+ return true;
42
+ });
43
+
44
+ if (provided.length === 0) {
45
+ throw new Error(`Provide at least one selection argument: ${keys.join(', ')}.`);
46
+ }
47
+
48
+ for (const key of provided) {
49
+ payload[key] = args[key];
50
+ }
51
+ return provided;
52
+ },
53
+ };
package/src/mcp/server.js CHANGED
@@ -14,6 +14,8 @@ export class TestomatioMCPServer {
14
14
  tools = TOOL_DEFINITIONS,
15
15
  name = 'testomatio-mcp-server',
16
16
  registryOptions = {},
17
+ version,
18
+ jsonSchemaValidator,
17
19
  }) {
18
20
  this.config = config;
19
21
  this.apiClient = apiClient;
@@ -26,17 +28,19 @@ export class TestomatioMCPServer {
26
28
  tools,
27
29
  ...registryOptions,
28
30
  });
29
- this.cleanupStarted = false;
31
+ this.closePromise = null;
32
+ this.sessionCleanupPromise = null;
30
33
 
31
34
  this.server = new Server(
32
35
  {
33
36
  name,
34
- version: getPackageVersion(),
37
+ version: version || getPackageVersion(),
35
38
  },
36
39
  {
37
40
  capabilities: {
38
41
  tools: {},
39
42
  },
43
+ jsonSchemaValidator,
40
44
  }
41
45
  );
42
46
 
@@ -57,36 +61,52 @@ export class TestomatioMCPServer {
57
61
  });
58
62
  }
59
63
 
60
- async run() {
61
- const transport = new StdioServerTransport();
64
+ async connect(transport) {
62
65
  await this.server.connect(transport);
66
+ }
67
+
68
+ async run() {
69
+ await this.connect(new StdioServerTransport());
63
70
  this.installSessionCleanup();
64
71
  this.logger.info('Testomatio MCP server started');
65
72
  }
66
73
 
67
- installSessionCleanup() {
68
- const cleanup = async () => {
69
- if (this.cleanupStarted) {
70
- return;
71
- }
74
+ close() {
75
+ if (!this.closePromise) {
76
+ this.closePromise = (async () => {
77
+ try {
78
+ await this.server.close();
79
+ } finally {
80
+ await this.#stopSession();
81
+ }
82
+ })();
83
+ }
72
84
 
73
- this.cleanupStarted = true;
74
- await this.apiClient?.stopSession?.();
75
- };
85
+ return this.closePromise;
86
+ }
76
87
 
88
+ installSessionCleanup() {
77
89
  this.server.onclose = () => {
78
- void cleanup();
90
+ void this.#stopSession();
79
91
  };
80
92
 
81
93
  process.once('beforeExit', () => {
82
- void cleanup();
94
+ void this.#stopSession();
83
95
  });
84
96
 
85
97
  for (const signal of ['SIGINT', 'SIGTERM']) {
86
98
  process.once(signal, async () => {
87
- await cleanup();
99
+ await this.#stopSession();
88
100
  process.exit(0);
89
101
  });
90
102
  }
91
103
  }
104
+
105
+ #stopSession() {
106
+ if (!this.sessionCleanupPromise) {
107
+ this.sessionCleanupPromise = Promise.resolve().then(() => this.apiClient?.stopSession?.());
108
+ }
109
+
110
+ return this.sessionCleanupPromise;
111
+ }
92
112
  }
@@ -2,6 +2,7 @@ import { SYSTEM_TOOLS } from './definitions/system.js';
2
2
  import { PROJECT_TOOLS } from './definitions/projects.js';
3
3
  import { TESTS_TOOLS } from './definitions/tests.js';
4
4
  import { SUITES_TOOLS } from './definitions/suites.js';
5
+ import { SHARES_TOOLS } from './definitions/shares.js';
5
6
  import { RUNS_TOOLS } from './definitions/runs.js';
6
7
  import { TESTRUNS_TOOLS } from './definitions/testruns.js';
7
8
  import { RUNGROUPS_TOOLS } from './definitions/rungroups.js';
@@ -22,6 +23,7 @@ export const TOOL_DEFINITIONS = withCountGroupOptions(withListOptions([
22
23
  ...PROJECT_TOOLS,
23
24
  ...TESTS_TOOLS,
24
25
  ...SUITES_TOOLS,
26
+ ...SHARES_TOOLS,
25
27
  ...RUNS_TOOLS,
26
28
  ...TESTRUNS_TOOLS,
27
29
  ...RUNGROUPS_TOOLS,
@@ -8,6 +8,7 @@ import { attachmentMethods } from './registry/attachments.js';
8
8
  import { issueMethods } from './registry/issues.js';
9
9
  import { listingMethods } from './registry/listings.js';
10
10
  import { payloadMethods } from './registry/payloads.js';
11
+ import { shareMethods } from './registry/shares.js';
11
12
 
12
13
  function withPagination(payload) {
13
14
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
@@ -72,6 +73,7 @@ export class ToolRegistry {
72
73
  this.registerScopedIssueHandlers(handlers);
73
74
  this.registerScopedAttachmentHandlers(handlers);
74
75
  this.registerGlobalHandlers(handlers);
76
+ this.registerShareHandlers(handlers);
75
77
  for (const registerHandlers of this.handlerRegistrars) {
76
78
  registerHandlers.call(this, handlers);
77
79
  }
@@ -116,5 +118,6 @@ Object.assign(
116
118
  attachmentMethods,
117
119
  listingMethods,
118
120
  issueMethods,
119
- payloadMethods
121
+ payloadMethods,
122
+ shareMethods
120
123
  );