mcp-connectwise-psa 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -6,8 +6,7 @@ An MCP ([Model Context Protocol](https://modelcontextprotocol.io)) server for [C
6
6
  - **Time entries** — log time against tickets, review your own time
7
7
  - **Companies & contacts** — fast lookup (read-only)
8
8
  - **Configurations** — devices/assets with serials, IPs, OS, warranty (read-only)
9
- - **Role-based access control** — viewer / editor bearer tokens decide which tools a session sees
10
- - **Per-tech API keys (BYOK)** — techs supply their own ConnectWise member keys, so notes and time entries are attributed to the *actual person* in ConnectWise
9
+ - **Per-tech API keys (BYOK)** — each tech supplies their own ConnectWise member keys; ConnectWise enforces that member's security role, and notes and time entries are attributed to the *actual person*
11
10
  - **Transports** — stdio for local use, streamable HTTP for shared deployments; Docker image included
12
11
 
13
12
  ## Quick start (local, stdio)
@@ -49,12 +48,10 @@ A `clientId` is required by the ConnectWise API — register a (free) integratio
49
48
 
50
49
  ```bash
51
50
  CW_SITE=… CW_COMPANY_ID=… CW_CLIENT_ID=… \
52
- MCP_TOKENS_VIEWER="alice:$(openssl rand -hex 32)" \
53
- MCP_TOKENS_EDITOR="bot:$(openssl rand -hex 32)" \
54
51
  node dist/index.js --transport http --port 3000
55
52
  ```
56
53
 
57
- Or with Docker: `docker build -t mcp-connectwise-psa . && docker run -p 3000:3000 -e CW_SITE -e CW_COMPANY_ID -e CW_CLIENT_ID -e MCP_TOKENS_VIEWER -e MCP_TOKENS_EDITOR mcp-connectwise-psa`
54
+ Or with Docker: `docker build -t mcp-connectwise-psa . && docker run -p 3000:3000 -e CW_SITE -e CW_COMPANY_ID -e CW_CLIENT_ID mcp-connectwise-psa`
58
55
 
59
56
  | Route | Purpose |
60
57
  |---|---|
@@ -63,35 +60,29 @@ Or with Docker: `docker build -t mcp-connectwise-psa . && docker run -p 3000:300
63
60
 
64
61
  > Sessions are held in memory — run a single instance (or sticky sessions).
65
62
 
66
- ## Access control
63
+ ## Access control — bring your own keys (BYOK)
67
64
 
68
- ### Role tokens
65
+ Over HTTP there is **no MCP-level role system**. Each session presents its own ConnectWise member API keys, and ConnectWise itself is the access control: the member's security role decides what succeeds, and every note and time entry is attributed to that member.
69
66
 
70
- ```bash
71
- MCP_TOKENS_VIEWER="alice:tokA,bob:tokB" # read-only tools
72
- MCP_TOKENS_EDITOR="helpdesk-bot:tokC" # + create/update tickets, notes, time entries
73
- MCP_TOKENS_ADMIN="ops:tokD" # reserved for future destructive tools
74
- ```
75
-
76
- Clients send `Authorization: Bearer <token>`. Tools outside the role are not even registered for the session, a runtime guard re-checks every call, and session ids never carry privilege (each request re-authenticates; principal mismatch → 403). Labels appear in the audit log and allow per-person revocation. With no tokens configured the server runs in dev mode (full access + loud warning).
77
-
78
- ### Bring your own keys (BYOK) — recommended for techs
67
+ Send your keys on the initialize request (and on every subsequent request in the session):
79
68
 
80
- Send your own ConnectWise member API keys on the initialize request:
81
-
82
- ```
69
+ ```http
83
70
  x-cw-public-key: <public key>
84
71
  x-cw-private-key: <private key>
85
72
  x-cw-member-id: <your member identifier> (optional — enables "my tickets"/"my time")
86
73
  ```
87
74
 
88
- The session then acts as *you* in ConnectWise: your security role limits what succeeds, and every note and time entry is attributed to you. `CLIENT_CW_KEYS` controls the policy: `with-token` (default — a bearer token is still required), `open`, or `disabled`. Keys are never logged; sessions are bound to a SHA-256 hash of the pair.
75
+ - A request with no keys is rejected with `401`; both key headers are required together.
76
+ - Keys are never logged. A session is bound to a SHA-256 hash of the key pair; presenting a different pair on the same session id → `403`.
77
+ - Create member API keys in ConnectWise under **My Account → API Keys**. Each tech uses their own.
89
78
 
90
- Token-only sessions use the server-wide `CW_PUBLIC_KEY`/`CW_PRIVATE_KEY` (use an integration API member with a conservative security role — its writes are attributed to that integration member).
79
+ Local **stdio** is single-user and uses the `CW_PUBLIC_KEY`/`CW_PRIVATE_KEY` from the environment instead of headers.
91
80
 
92
81
  ## Tools
93
82
 
94
- | Tool | Tier |
83
+ The full tool surface is always available; whether a write succeeds is governed by the member's ConnectWise security role.
84
+
85
+ | Tool | Access |
95
86
  |---|---|
96
87
  | `cw_my_tickets`, `cw_search_tickets`, `cw_get_ticket` | read |
97
88
  | `cw_list_my_time` | read |
@@ -100,7 +91,7 @@ Token-only sessions use the server-wide `CW_PUBLIC_KEY`/`CW_PRIVATE_KEY` (use an
100
91
  | `cw_create_ticket`, `cw_update_ticket`, `cw_add_ticket_note` | write |
101
92
  | `cw_create_time_entry` | write |
102
93
 
103
- Viewer = read. Editor/Admin = read + write. No destructive (delete) tools in v1.
94
+ No destructive (delete) tools in v1.
104
95
 
105
96
  ## Configuration reference
106
97
 
@@ -109,11 +100,9 @@ Viewer = read. Editor/Admin = read + write. No destructive (delete) tools in v1.
109
100
  | `CW_SITE` | — | ConnectWise host (cloud or on-prem; full URLs accepted) |
110
101
  | `CW_COMPANY_ID` | — | Login company id |
111
102
  | `CW_CLIENT_ID` | — | Integration clientId |
112
- | `CW_PUBLIC_KEY` / `CW_PRIVATE_KEY` | — | Server-wide API member keys (optional when BYOK is enabled) |
113
- | `CW_MEMBER_IDENTIFIER` | — | Member the server-wide keys belong to |
103
+ | `CW_PUBLIC_KEY` / `CW_PRIVATE_KEY` | — | API member keys required for stdio; unused on HTTP (BYOK) |
104
+ | `CW_MEMBER_IDENTIFIER` | — | Member the stdio keys belong to (my-tickets/my-time) |
114
105
  | `TRANSPORT` / `PORT` | `stdio` / `3000` | Transport selection |
115
- | `CLIENT_CW_KEYS` | `with-token` | BYOK policy: `disabled`, `with-token`, `open` |
116
- | `MCP_TOKENS_VIEWER/EDITOR/ADMIN` | — | `label:token,label:token` per role |
117
106
 
118
107
  ## Notes & limits
119
108
 
package/dist/config.js CHANGED
@@ -7,20 +7,18 @@
7
7
  * path are stripped.
8
8
  * CW_COMPANY_ID ConnectWise login company id (required)
9
9
  * CW_CLIENT_ID Integration clientId from developer.connectwise.com (required)
10
- * CW_PUBLIC_KEY Server-wide API member public key (optional enables
11
- * CW_PRIVATE_KEY token-only sessions; omit to require per-tech keys)
12
- * CW_MEMBER_IDENTIFIER Member identifier the server-wide keys belong to
13
- * (enables "my tickets"/"my time" for token-only sessions)
10
+ * CW_PUBLIC_KEY API member public key — required for stdio; unused on
11
+ * CW_PRIVATE_KEY HTTP, where each session brings its own keys (BYOK)
12
+ * CW_MEMBER_IDENTIFIER Member the stdio keys belong to (enables
13
+ * "my tickets"/"my time")
14
14
  * TRANSPORT stdio | http (default: stdio)
15
15
  * PORT HTTP port (default: 3000)
16
- * CLIENT_CW_KEYS disabled | with-token | open (default: with-token) —
17
- * whether HTTP clients may supply their own member API
18
- * keys via x-cw-public-key / x-cw-private-key headers
19
- * MCP_TOKENS_VIEWER Comma-separated label:token list for the viewer role
20
- * MCP_TOKENS_EDITOR Comma-separated label:token list for the editor role
21
- * MCP_TOKENS_ADMIN Comma-separated label:token list for the admin role
16
+ *
17
+ * Access model: stdio uses the server-wide keys above (single local user). HTTP
18
+ * sessions each bring their own member API keys (BYOK) via x-cw-public-key /
19
+ * x-cw-private-key headers; ConnectWise enforces that member's security role.
20
+ * There is no MCP-level role gating.
22
21
  */
23
- const CLIENT_KEY_MODES = ["disabled", "with-token", "open"];
24
22
  export class ConfigError extends Error {
25
23
  }
26
24
  function flagValue(argv, name) {
@@ -63,10 +61,6 @@ export function loadConfig(argv = process.argv.slice(2), env = process.env) {
63
61
  if (!clientId) {
64
62
  throw new ConfigError("CW_CLIENT_ID is required — register an integration at developer.connectwise.com");
65
63
  }
66
- const clientKeyMode = (env.CLIENT_CW_KEYS || "with-token");
67
- if (!CLIENT_KEY_MODES.includes(clientKeyMode)) {
68
- throw new ConfigError(`Invalid CLIENT_CW_KEYS "${clientKeyMode}" — expected one of: ${CLIENT_KEY_MODES.join(", ")}`);
69
- }
70
64
  const publicKey = env.CW_PUBLIC_KEY || undefined;
71
65
  const privateKey = env.CW_PRIVATE_KEY || undefined;
72
66
  if (!!publicKey !== !!privateKey) {
@@ -75,9 +69,6 @@ export function loadConfig(argv = process.argv.slice(2), env = process.env) {
75
69
  if (transport === "stdio" && !publicKey) {
76
70
  throw new ConfigError("CW_PUBLIC_KEY / CW_PRIVATE_KEY are required for stdio transport");
77
71
  }
78
- if (transport === "http" && !publicKey && clientKeyMode === "disabled") {
79
- throw new ConfigError("CW_PUBLIC_KEY / CW_PRIVATE_KEY are required when CLIENT_CW_KEYS=disabled — there are no keys the server could use");
80
- }
81
72
  return {
82
73
  transport,
83
74
  port,
@@ -87,6 +78,5 @@ export function loadConfig(argv = process.argv.slice(2), env = process.env) {
87
78
  publicKey,
88
79
  privateKey,
89
80
  memberIdentifier: env.CW_MEMBER_IDENTIFIER || undefined,
90
- clientKeyMode,
91
81
  };
92
82
  }
package/dist/http/app.js CHANGED
@@ -1,27 +1,23 @@
1
1
  /**
2
2
  * HTTP transport: Express app exposing
3
3
  *
4
- * POST/GET/DELETE /mcp MCP streamable-http endpoint (auth + RBAC)
4
+ * POST/GET/DELETE /mcp MCP streamable-http endpoint (BYOK auth)
5
5
  * GET /health liveness probe
6
6
  *
7
- * Authentication model:
8
- * - Role tokens (Authorization: Bearer …) map to viewer/editor/admin and gate
9
- * which tools a session gets, using the server-wide ConnectWise keys.
10
- * - Bring-your-own-keys: x-cw-public-key + x-cw-private-key headers bind the
11
- * session to that member's API keys with the FULL tool surface — the
12
- * member's own ConnectWise security role is the access control, and writes
13
- * (notes, time entries) are attributed to that member. Optional
14
- * x-cw-member-id header names the member for "my tickets"/"my time".
15
- * CLIENT_CW_KEYS controls the policy (with-token default / open / disabled).
7
+ * Authentication model — bring-your-own-keys only:
8
+ * - Every session presents its own ConnectWise member API keys via the
9
+ * x-cw-public-key + x-cw-private-key headers. Those keys are both the
10
+ * credential and the permission model: ConnectWise enforces that member's
11
+ * security role, and writes (notes, time entries) are attributed to that
12
+ * member. Optional x-cw-member-id names the member for "my tickets"/"my time".
13
+ * - The full tool surface is exposed; there is no MCP-level role gating.
16
14
  * - A session id never carries privilege: every request re-authenticates and
17
- * must present the same principal (role + label + key hash) the session was
18
- * created with.
15
+ * must present the same key pair (SHA-256 hash) the session was created with.
19
16
  */
20
17
  import { createHash, randomUUID } from "node:crypto";
21
18
  import express from "express";
22
19
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
23
20
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
24
- import { authenticateToken, loadTokenEntries } from "../auth/tokens.js";
25
21
  import { createServer, SERVER_NAME, SERVER_VERSION } from "../server.js";
26
22
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
27
23
  function rpcError(res, status, code, message) {
@@ -38,62 +34,30 @@ function headerValue(req, name) {
38
34
  return Array.isArray(value) ? value[0] : value;
39
35
  }
40
36
  /** Resolve the principal for a request. Exported for tests. */
41
- export function resolveAuth(req, config) {
42
- const entries = loadTokenEntries();
43
- const token = authenticateToken(req.headers.authorization, entries);
37
+ export function resolveAuth(req) {
44
38
  const clientPublic = headerValue(req, "x-cw-public-key");
45
39
  const clientPrivate = headerValue(req, "x-cw-private-key");
46
40
  const clientMember = headerValue(req, "x-cw-member-id");
47
- if (clientPublic || clientPrivate) {
48
- if (config.clientKeyMode === "disabled") {
49
- return unauthorized("Client-supplied ConnectWise keys are disabled on this server (CLIENT_CW_KEYS=disabled).");
50
- }
51
- if (!clientPublic || !clientPrivate) {
52
- return unauthorized("Both x-cw-public-key and x-cw-private-key headers are required.");
53
- }
54
- const keyHash = sha256(`${clientPublic}:${clientPrivate}`);
55
- const byokLabel = `byok:${keyHash.slice(0, 8)}`;
56
- if (config.clientKeyMode === "with-token" && entries.length > 0 && !token) {
57
- return unauthorized("A valid bearer token is required alongside your ConnectWise keys (CLIENT_CW_KEYS=with-token).");
58
- }
59
- const label = token ? `${token.label}+${byokLabel}` : byokLabel;
60
- // BYOK sessions get the full tool surface; the member's own ConnectWise
61
- // security role is the effective access control.
62
- return {
63
- ok: true,
64
- role: "admin",
65
- label,
66
- credentials: {
67
- publicKey: clientPublic,
68
- privateKey: clientPrivate,
69
- memberIdentifier: clientMember,
70
- },
71
- keyHash,
72
- };
41
+ if (!clientPublic && !clientPrivate) {
42
+ return unauthorized("Supply your ConnectWise member API keys via the x-cw-public-key and x-cw-private-key headers.");
73
43
  }
74
- const serverCredentials = config.publicKey && config.privateKey
75
- ? {
76
- publicKey: config.publicKey,
77
- privateKey: config.privateKey,
78
- memberIdentifier: config.memberIdentifier,
79
- }
80
- : null;
81
- if (entries.length === 0) {
82
- if (!serverCredentials) {
83
- return unauthorized("No ConnectWise keys available — set CW_PUBLIC_KEY/CW_PRIVATE_KEY on the server or send x-cw-public-key/x-cw-private-key.");
84
- }
85
- return { ok: true, role: "admin", label: "dev-unauthenticated", credentials: serverCredentials, keyHash: null };
44
+ if (!clientPublic || !clientPrivate) {
45
+ return unauthorized("Both x-cw-public-key and x-cw-private-key headers are required.");
86
46
  }
87
- if (!token) {
88
- return unauthorized("Unauthorized: invalid or missing bearer token.");
89
- }
90
- if (!serverCredentials) {
91
- return unauthorized("This server has no shared ConnectWise keys — supply your own via the x-cw-public-key/x-cw-private-key headers.");
92
- }
93
- return { ok: true, role: token.role, label: token.label, credentials: serverCredentials, keyHash: null };
47
+ const keyHash = sha256(`${clientPublic}:${clientPrivate}`);
48
+ return {
49
+ ok: true,
50
+ label: `byok:${keyHash.slice(0, 8)}`,
51
+ credentials: {
52
+ publicKey: clientPublic,
53
+ privateKey: clientPrivate,
54
+ memberIdentifier: clientMember,
55
+ },
56
+ keyHash,
57
+ };
94
58
  }
95
59
  function principalMatches(session, auth) {
96
- return (session.role === auth.role && session.label === auth.label && session.keyHash === auth.keyHash);
60
+ return session.keyHash === auth.keyHash;
97
61
  }
98
62
  export function createApp(config) {
99
63
  const app = express();
@@ -104,7 +68,7 @@ export function createApp(config) {
104
68
  });
105
69
  app.post("/mcp", (req, res) => {
106
70
  void (async () => {
107
- const auth = resolveAuth(req, config);
71
+ const auth = resolveAuth(req);
108
72
  if (!auth.ok)
109
73
  return rpcError(res, auth.status, auth.code, auth.message);
110
74
  const sessionId = headerValue(req, "mcp-session-id");
@@ -126,11 +90,10 @@ export function createApp(config) {
126
90
  onsessioninitialized: (newSessionId) => {
127
91
  sessions.set(newSessionId, {
128
92
  transport,
129
- role: auth.role,
130
93
  label: auth.label,
131
94
  keyHash: auth.keyHash,
132
95
  });
133
- console.error(`[rbac] session ${newSessionId} created for ${auth.label} (${auth.role})`);
96
+ console.error(`[auth] session ${newSessionId} created for ${auth.label}`);
134
97
  },
135
98
  });
136
99
  transport.onclose = () => {
@@ -138,7 +101,6 @@ export function createApp(config) {
138
101
  sessions.delete(transport.sessionId);
139
102
  };
140
103
  const server = createServer(config, {
141
- role: auth.role,
142
104
  label: auth.label,
143
105
  credentials: auth.credentials,
144
106
  });
@@ -152,7 +114,7 @@ export function createApp(config) {
152
114
  });
153
115
  const handleSessionRequest = (req, res) => {
154
116
  void (async () => {
155
- const auth = resolveAuth(req, config);
117
+ const auth = resolveAuth(req);
156
118
  if (!auth.ok)
157
119
  return rpcError(res, auth.status, auth.code, auth.message);
158
120
  const sessionId = headerValue(req, "mcp-session-id");
package/dist/index.js CHANGED
@@ -2,7 +2,6 @@
2
2
  /** CLI entry point — runs the MCP server over stdio (default) or HTTP. */
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { ConfigError, loadConfig } from "./config.js";
5
- import { loadTokenEntries } from "./auth/tokens.js";
6
5
  import { createServer, SERVER_NAME, SERVER_VERSION } from "./server.js";
7
6
  import { createApp } from "./http/app.js";
8
7
  const USAGE = `${SERVER_NAME} v${SERVER_VERSION}
@@ -19,31 +18,23 @@ Environment:
19
18
  CW_SITE ConnectWise host (e.g. na.myconnectwise.net)
20
19
  CW_COMPANY_ID Login company id
21
20
  CW_CLIENT_ID Integration clientId (developer.connectwise.com)
22
- CW_PUBLIC_KEY Server-wide API member public key (optional with BYOK)
23
- CW_PRIVATE_KEY Server-wide API member private key
24
- CW_MEMBER_IDENTIFIER Member the server-wide keys belong to (my-tickets/my-time)
25
- MCP_TOKENS_VIEWER label:token[,label:token…] — read-only access (HTTP)
26
- MCP_TOKENS_EDITOR label:token[,…] read + tickets/notes/time writes
27
- MCP_TOKENS_ADMIN label:token[,…] reserved for future destructive tools
28
- CLIENT_CW_KEYS disabled|with-token|open client-supplied CW keys (default: with-token)
21
+ CW_PUBLIC_KEY API member public key (required for stdio)
22
+ CW_PRIVATE_KEY API member private key (required for stdio)
23
+ CW_MEMBER_IDENTIFIER Member the stdio keys belong to (my-tickets/my-time)
24
+
25
+ HTTP sessions authenticate per-request with their own member keys via the
26
+ x-cw-public-key / x-cw-private-key headers (BYOK); the CW_* keys above are used
27
+ only by stdio.
29
28
  `;
30
29
  function logStartupSummary(config) {
31
- const entries = loadTokenEntries();
32
- if (entries.length === 0) {
33
- console.error("[auth] WARNING: no MCP_TOKENS_* configured — all HTTP requests are accepted with FULL access. " +
34
- "Set MCP_TOKENS_VIEWER / MCP_TOKENS_EDITOR / MCP_TOKENS_ADMIN in production.");
35
- }
36
- else {
37
- console.error(`[auth] ${entries.length} token(s) configured: ${entries.map((e) => `${e.label}(${e.role})`).join(", ")}`);
30
+ console.error("[auth] HTTP: each session must present its own ConnectWise keys via " +
31
+ "x-cw-public-key / x-cw-private-key (BYOK); ConnectWise enforces the member's security role.");
32
+ if (config.publicKey) {
33
+ console.error("[cw] CW_PUBLIC_KEY/CW_PRIVATE_KEY are set but unused on HTTP — sessions use their own keys.");
38
34
  }
39
- console.error(`[auth] Client-supplied ConnectWise keys: ${config.clientKeyMode}`);
40
- console.error(config.publicKey
41
- ? `[cw] Server-wide keys configured${config.memberIdentifier ? ` (member: ${config.memberIdentifier})` : " (no CW_MEMBER_IDENTIFIER — my-tickets/my-time unavailable for token-only sessions)"}`
42
- : "[cw] No server-wide keys — sessions must bring their own via x-cw-public-key/x-cw-private-key.");
43
35
  }
44
36
  async function runStdio(config) {
45
37
  const server = createServer(config, {
46
- role: "admin",
47
38
  label: "stdio",
48
39
  credentials: {
49
40
  publicKey: config.publicKey,
package/dist/server.js CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Builds an McpServer for one session. A session is defined by:
3
- * - the ConnectWise credentials it uses (server-wide keys or client-supplied)
4
- * - the role that gates which tools are registered
2
+ * Builds an McpServer for one session. A session is defined by the ConnectWise
3
+ * credentials it uses (server-wide keys on stdio, or client-supplied via BYOK).
4
+ * The full tool surface is always registered; the member's ConnectWise security
5
+ * role is the access control.
5
6
  */
6
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
8
  import { createRequire } from "node:module";
8
- import { ToolRegistrar } from "./auth/roles.js";
9
+ import { ToolRegistrar } from "./tools/registrar.js";
9
10
  import { CWClient } from "./cw/client.js";
10
11
  import { registerTicketTools } from "./tools/tickets.js";
11
12
  import { registerTimeTools } from "./tools/time.js";
@@ -31,7 +32,7 @@ const INSTRUCTIONS = `# ConnectWise PSA MCP server
31
32
 
32
33
  ## Notes
33
34
  - Lists are paginated; ask for more pages rather than huge page sizes.
34
- - Some tools may be unavailable depending on this session's access level.`;
35
+ - A write may still fail if your ConnectWise security role forbids it.`;
35
36
  export function createServer(config, session) {
36
37
  const client = new CWClient({
37
38
  site: config.site,
@@ -40,7 +41,7 @@ export function createServer(config, session) {
40
41
  credentials: session.credentials,
41
42
  });
42
43
  const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { instructions: INSTRUCTIONS });
43
- const reg = new ToolRegistrar(server, session.role);
44
+ const reg = new ToolRegistrar(server);
44
45
  registerTicketTools(reg, client);
45
46
  registerTimeTools(reg, client);
46
47
  registerCompanyTools(reg, client);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Tool registration.
3
+ *
4
+ * Every tool is registered on the session's McpServer. There is no MCP-level
5
+ * role gating: each session authenticates with a ConnectWise API member key
6
+ * (its own, via BYOK, or the server-wide keys on stdio), and ConnectWise
7
+ * enforces that member's security role server-side. The MCP server exposes the
8
+ * full tool surface and lets the CW API be the access control.
9
+ */
10
+ /** Registers tools on an McpServer. */
11
+ export class ToolRegistrar {
12
+ server;
13
+ constructor(server) {
14
+ this.server = server;
15
+ }
16
+ register(spec, handler) {
17
+ this.server.registerTool(spec.name, {
18
+ title: spec.title,
19
+ description: spec.description,
20
+ inputSchema: spec.inputSchema,
21
+ annotations: spec.annotations,
22
+ }, handler);
23
+ }
24
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-connectwise-psa",
3
- "version": "0.1.0",
4
- "description": "MCP server for ConnectWise PSA (Manage) — tickets, time entries, companies, and configurations for technicians, with role-based access control and per-tech API keys",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for ConnectWise PSA (Manage) — tickets, time entries, companies, and configurations for technicians, with per-tech API keys (BYOK) so writes are attributed to the real member",
5
5
  "mcpName": "io.github.selic/mcp-connectwise-psa",
6
6
  "type": "module",
7
7
  "repository": {
@@ -41,7 +41,7 @@
41
41
  "psa",
42
42
  "msp",
43
43
  "tickets",
44
- "rbac"
44
+ "byok"
45
45
  ],
46
46
  "engines": {
47
47
  "node": ">=20"