syncromsp-mcp 0.1.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/LICENSE +21 -0
- package/README.md +244 -0
- package/dist/api-client.d.ts +21 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +117 -0
- package/dist/api-client.js.map +1 -0
- package/dist/auth.d.ts +37 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +133 -0
- package/dist/auth.js.map +1 -0
- package/dist/domains/admin.d.ts +4 -0
- package/dist/domains/admin.d.ts.map +1 -0
- package/dist/domains/admin.js +807 -0
- package/dist/domains/admin.js.map +1 -0
- package/dist/domains/appointments.d.ts +4 -0
- package/dist/domains/appointments.d.ts.map +1 -0
- package/dist/domains/appointments.js +231 -0
- package/dist/domains/appointments.js.map +1 -0
- package/dist/domains/assets.d.ts +4 -0
- package/dist/domains/assets.d.ts.map +1 -0
- package/dist/domains/assets.js +138 -0
- package/dist/domains/assets.js.map +1 -0
- package/dist/domains/contacts.d.ts +4 -0
- package/dist/domains/contacts.d.ts.map +1 -0
- package/dist/domains/contacts.js +148 -0
- package/dist/domains/contacts.js.map +1 -0
- package/dist/domains/contracts.d.ts +4 -0
- package/dist/domains/contracts.d.ts.map +1 -0
- package/dist/domains/contracts.js +125 -0
- package/dist/domains/contracts.js.map +1 -0
- package/dist/domains/customers.d.ts +4 -0
- package/dist/domains/customers.d.ts.map +1 -0
- package/dist/domains/customers.js +323 -0
- package/dist/domains/customers.js.map +1 -0
- package/dist/domains/estimates.d.ts +4 -0
- package/dist/domains/estimates.d.ts.map +1 -0
- package/dist/domains/estimates.js +262 -0
- package/dist/domains/estimates.js.map +1 -0
- package/dist/domains/index.d.ts +5 -0
- package/dist/domains/index.d.ts.map +1 -0
- package/dist/domains/index.js +34 -0
- package/dist/domains/index.js.map +1 -0
- package/dist/domains/invoices.d.ts +4 -0
- package/dist/domains/invoices.d.ts.map +1 -0
- package/dist/domains/invoices.js +292 -0
- package/dist/domains/invoices.js.map +1 -0
- package/dist/domains/leads.d.ts +4 -0
- package/dist/domains/leads.d.ts.map +1 -0
- package/dist/domains/leads.js +135 -0
- package/dist/domains/leads.js.map +1 -0
- package/dist/domains/payments.d.ts +4 -0
- package/dist/domains/payments.d.ts.map +1 -0
- package/dist/domains/payments.js +188 -0
- package/dist/domains/payments.js.map +1 -0
- package/dist/domains/products.d.ts +4 -0
- package/dist/domains/products.d.ts.map +1 -0
- package/dist/domains/products.js +350 -0
- package/dist/domains/products.js.map +1 -0
- package/dist/domains/rmm.d.ts +4 -0
- package/dist/domains/rmm.d.ts.map +1 -0
- package/dist/domains/rmm.js +100 -0
- package/dist/domains/rmm.js.map +1 -0
- package/dist/domains/scheduling.d.ts +4 -0
- package/dist/domains/scheduling.d.ts.map +1 -0
- package/dist/domains/scheduling.js +206 -0
- package/dist/domains/scheduling.js.map +1 -0
- package/dist/domains/tickets.d.ts +4 -0
- package/dist/domains/tickets.d.ts.map +1 -0
- package/dist/domains/tickets.js +533 -0
- package/dist/domains/tickets.js.map +1 -0
- package/dist/domains/time.d.ts +4 -0
- package/dist/domains/time.d.ts.map +1 -0
- package/dist/domains/time.js +93 -0
- package/dist/domains/time.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +137 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +183 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +12 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +31 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +44 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/pagination.d.ts +12 -0
- package/dist/utils/pagination.d.ts.map +1 -0
- package/dist/utils/pagination.js +18 -0
- package/dist/utils/pagination.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +15 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +63 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/validators.d.ts +9 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +59 -0
- package/dist/utils/validators.js.map +1 -0
- package/dist/utils/version-check.d.ts +7 -0
- package/dist/utils/version-check.d.ts.map +1 -0
- package/dist/utils/version-check.js +59 -0
- package/dist/utils/version-check.js.map +1 -0
- package/manifest.json +64 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris
|
|
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
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# SyncroMSP MCP Server
|
|
2
|
+
|
|
3
|
+
A fully-featured [Model Context Protocol](https://modelcontextprotocol.io) server for the [SyncroMSP](https://syncromsp.com) IT/MSP platform. Provides AI assistants with access to tickets, customers, assets, invoices, and 30+ resource types through a domain-navigation architecture that keeps token usage efficient.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **170 API endpoints** across 15 lazy-loaded domains
|
|
8
|
+
- **Domain navigation** — only 3-8 tools visible at a time, not 170
|
|
9
|
+
- **Full CRUD** for tickets, customers, invoices, estimates, appointments, contracts, products, and more
|
|
10
|
+
- **Ticket comments** — email replies, public notes, and private/internal notes
|
|
11
|
+
- **Line items** — add products from catalog or manual entries to tickets, invoices, estimates, schedules
|
|
12
|
+
- **RMM alerts** — create, read, mute, resolve alerts on assets
|
|
13
|
+
- **Rate limiting** — built-in 180 req/min token bucket (Syncro API limit)
|
|
14
|
+
- **Confirmation required** for all destructive operations (DELETE, etc.)
|
|
15
|
+
- **Docker deployment** with OAuth2 proxy for remote/team access
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### Claude Code
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
claude mcp add syncromsp \
|
|
23
|
+
--env SYNCRO_API_KEY=your-api-key \
|
|
24
|
+
--env SYNCRO_SUBDOMAIN=your-subdomain \
|
|
25
|
+
-- npx syncromsp-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Claude Desktop
|
|
29
|
+
|
|
30
|
+
#### Option 1: MCPB Extension (Recommended)
|
|
31
|
+
|
|
32
|
+
Download the latest `.mcpb` file from [Releases](https://github.com/advenimus/syncromsp-mcp/releases) and double-click to install. Claude Desktop will prompt you for your API key and subdomain.
|
|
33
|
+
|
|
34
|
+
#### Option 2: Manual Configuration
|
|
35
|
+
|
|
36
|
+
Add to your `claude_desktop_config.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"syncromsp": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["syncromsp-mcp"],
|
|
44
|
+
"env": {
|
|
45
|
+
"SYNCRO_API_KEY": "your-api-key",
|
|
46
|
+
"SYNCRO_SUBDOMAIN": "your-subdomain"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Config file location:
|
|
54
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
55
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
56
|
+
|
|
57
|
+
### From Source
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/advenimus/syncromsp-mcp.git
|
|
61
|
+
cd syncromsp-mcp
|
|
62
|
+
npm install
|
|
63
|
+
npm run build
|
|
64
|
+
|
|
65
|
+
# Set environment variables
|
|
66
|
+
export SYNCRO_API_KEY=your-api-key
|
|
67
|
+
export SYNCRO_SUBDOMAIN=your-subdomain
|
|
68
|
+
|
|
69
|
+
# Run
|
|
70
|
+
npm start
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Getting Your API Key
|
|
74
|
+
|
|
75
|
+
1. Log in to your Syncro account
|
|
76
|
+
2. Go to **Admin** > **API Tokens**
|
|
77
|
+
3. Click **+ New Token**
|
|
78
|
+
4. Select the **Custom Permissions** tab
|
|
79
|
+
5. Name your token and set permissions for the resources you need
|
|
80
|
+
6. Click **Create** and copy the token (it cannot be retrieved later)
|
|
81
|
+
|
|
82
|
+
Your subdomain is the part before `.syncromsp.com` in your Syncro URL (e.g., `mycompany` from `mycompany.syncromsp.com`).
|
|
83
|
+
|
|
84
|
+
## How It Works
|
|
85
|
+
|
|
86
|
+
### Domain Navigation
|
|
87
|
+
|
|
88
|
+
Instead of loading 170+ tools at once (which would overwhelm any AI), the server uses a **navigation pattern**:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
Startup → 3 tools visible:
|
|
92
|
+
syncro_navigate("tickets") → loads ticket tools
|
|
93
|
+
syncro_status() → shows current domain
|
|
94
|
+
syncro_back() → returns to root
|
|
95
|
+
|
|
96
|
+
After navigating to "tickets" → domain tools visible:
|
|
97
|
+
tickets_list, tickets_get, tickets_create, tickets_update,
|
|
98
|
+
tickets_delete, tickets_comment, tickets_add_line_item, ...
|
|
99
|
+
syncro_back()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Available Domains
|
|
103
|
+
|
|
104
|
+
| Domain | Description | Key Operations |
|
|
105
|
+
|--------|-------------|---------------|
|
|
106
|
+
| **tickets** | Service tickets | CRUD, comments (email/public/private), line items, timers, attachments |
|
|
107
|
+
| **customers** | Customer records | CRUD, phone numbers, autocomplete |
|
|
108
|
+
| **assets** | Customer assets | CRUD, patches, properties (OS, RAM, HDD, etc.) |
|
|
109
|
+
| **contacts** | Customer contacts | CRUD |
|
|
110
|
+
| **invoices** | Invoices | CRUD, line items (manual + product catalog), print, email |
|
|
111
|
+
| **estimates** | Estimates/quotes | CRUD, line items, print, email, convert to invoice |
|
|
112
|
+
| **appointments** | Calendar appointments | CRUD, appointment types, ticket linking |
|
|
113
|
+
| **products** | Inventory/products | CRUD, serials, SKUs, categories, images |
|
|
114
|
+
| **payments** | Payment records | Create, read, multi-invoice distribution |
|
|
115
|
+
| **leads** | Leads/opportunities | Create, read, update |
|
|
116
|
+
| **contracts** | Service contracts | CRUD |
|
|
117
|
+
| **rmm** | RMM alerts | Create, read, mute, resolve |
|
|
118
|
+
| **scheduling** | Recurring invoices | CRUD, schedule line items |
|
|
119
|
+
| **time** | Timers and time logs | List, update |
|
|
120
|
+
| **admin** | Search, users, vendors, wiki, portal, settings, purchase orders, and more | Various |
|
|
121
|
+
|
|
122
|
+
## Docker Deployment (Remote MCP with OAuth)
|
|
123
|
+
|
|
124
|
+
For connecting Claude.ai or other remote MCP clients with built-in OAuth 2.1 authentication:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
cp .env.example .env
|
|
128
|
+
# Edit .env with your Syncro credentials and base URL
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Set `MCP_BASE_URL` to your public HTTPS URL (required for OAuth):
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
SYNCRO_API_KEY=your-api-key
|
|
135
|
+
SYNCRO_SUBDOMAIN=your-subdomain
|
|
136
|
+
MCP_BASE_URL=https://mcp.yourcompany.com
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Then deploy:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
docker compose up -d
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Connecting Claude.ai
|
|
146
|
+
|
|
147
|
+
1. Deploy the server with HTTPS (via reverse proxy like Traefik, Caddy, or nginx)
|
|
148
|
+
2. In Claude.ai, go to **Settings** > **MCP Servers** > **Add Remote Server**
|
|
149
|
+
3. Enter your MCP URL: `https://mcp.yourcompany.com/mcp`
|
|
150
|
+
4. Claude.ai will auto-discover the OAuth endpoints and authenticate
|
|
151
|
+
|
|
152
|
+
The server implements the full MCP OAuth 2.1 + PKCE spec:
|
|
153
|
+
- `/.well-known/oauth-authorization-server` — discovery metadata
|
|
154
|
+
- `/authorize` — authorization endpoint (auto-approves since you control the server)
|
|
155
|
+
- `/token` — token endpoint with PKCE S256 validation
|
|
156
|
+
- `/register` — dynamic client registration (RFC 7591)
|
|
157
|
+
- Bearer token validation on all MCP requests
|
|
158
|
+
|
|
159
|
+
### Disabling Auth
|
|
160
|
+
|
|
161
|
+
For testing or private networks, disable OAuth:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
MCP_AUTH=false docker compose up -d
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Environment Variables
|
|
168
|
+
|
|
169
|
+
| Variable | Required | Description |
|
|
170
|
+
|----------|----------|-------------|
|
|
171
|
+
| `SYNCRO_API_KEY` | Yes | Your Syncro API token |
|
|
172
|
+
| `SYNCRO_SUBDOMAIN` | Yes | Your Syncro subdomain |
|
|
173
|
+
| `MCP_TRANSPORT` | No | `stdio` (default) or `http` |
|
|
174
|
+
| `MCP_PORT` | No | HTTP port (default: `8080`) |
|
|
175
|
+
| `MCP_BASE_URL` | For OAuth | Public HTTPS URL (e.g., `https://mcp.yourcompany.com`) |
|
|
176
|
+
| `MCP_AUTH` | No | `true` (default) or `false` to disable OAuth |
|
|
177
|
+
| `MCP_TOOL_MODE` | No | `flat` (default, all tools) or `navigation` (lazy domains) |
|
|
178
|
+
|
|
179
|
+
## API Rate Limits
|
|
180
|
+
|
|
181
|
+
Syncro enforces a rate limit of **180 requests per minute per IP**. The server includes a built-in token bucket rate limiter that automatically queues requests when approaching the limit.
|
|
182
|
+
|
|
183
|
+
## Important Notes
|
|
184
|
+
|
|
185
|
+
- **Destructive operations** (DELETE, remove line item, etc.) require explicit confirmation
|
|
186
|
+
- **Line items** cannot be added inline during resource creation — always add them via separate API calls after creating the parent resource
|
|
187
|
+
- **Ticket comments** have 3 modes: email reply, public note, and private/internal note
|
|
188
|
+
- Some resources have no DELETE endpoint (vendors, leads, products, assets) — use `disabled: true` via update instead
|
|
189
|
+
|
|
190
|
+
## Staying Up to Date
|
|
191
|
+
|
|
192
|
+
The server checks for updates on startup and logs a warning if a newer version is available.
|
|
193
|
+
|
|
194
|
+
### npx
|
|
195
|
+
|
|
196
|
+
Always uses the latest published version automatically:
|
|
197
|
+
```bash
|
|
198
|
+
npx syncromsp-mcp@latest
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Claude Desktop (MCPB)
|
|
202
|
+
|
|
203
|
+
Download the latest `.mcpb` from [Releases](https://github.com/advenimus/syncromsp-mcp/releases) and reinstall.
|
|
204
|
+
|
|
205
|
+
### Docker
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
docker compose pull
|
|
209
|
+
docker compose up -d
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
For **automatic updates**, add [Watchtower](https://containrrr.dev/watchtower/):
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
services:
|
|
216
|
+
watchtower:
|
|
217
|
+
image: containrrr/watchtower
|
|
218
|
+
volumes:
|
|
219
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
220
|
+
environment:
|
|
221
|
+
- WATCHTOWER_POLL_INTERVAL=86400 # Check daily
|
|
222
|
+
- WATCHTOWER_CLEANUP=true
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### From Source
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
git pull
|
|
229
|
+
npm install
|
|
230
|
+
npm run build
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Development
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
npm run dev # Run with tsx (hot reload)
|
|
237
|
+
npm run build # Compile TypeScript
|
|
238
|
+
npm test # Run tests
|
|
239
|
+
npm run lint # Lint source
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SyncroApiConfig, SyncroErrorResponse } from "./types.js";
|
|
2
|
+
export declare class SyncroApiError extends Error {
|
|
3
|
+
readonly statusCode: number;
|
|
4
|
+
readonly details?: SyncroErrorResponse | undefined;
|
|
5
|
+
constructor(message: string, statusCode: number, details?: SyncroErrorResponse | undefined);
|
|
6
|
+
}
|
|
7
|
+
export declare class SyncroApiClient {
|
|
8
|
+
private readonly baseUrl;
|
|
9
|
+
private readonly apiKey;
|
|
10
|
+
private readonly rateLimiter;
|
|
11
|
+
constructor(config: SyncroApiConfig);
|
|
12
|
+
private buildUrl;
|
|
13
|
+
private request;
|
|
14
|
+
private formatErrorMessage;
|
|
15
|
+
get<T>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
16
|
+
post<T>(path: string, body?: Record<string, unknown>, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
17
|
+
put<T>(path: string, body?: Record<string, unknown>, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
18
|
+
patch<T>(path: string, body?: Record<string, unknown>, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
19
|
+
delete<T>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=api-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAGvE,qBAAa,cAAe,SAAQ,KAAK;aAGrB,UAAU,EAAE,MAAM;aAClB,OAAO,CAAC,EAAE,mBAAmB;gBAF7C,OAAO,EAAE,MAAM,EACC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,mBAAmB,YAAA;CAKhD;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;gBAE9B,MAAM,EAAE,eAAe;IAQnC,OAAO,CAAC,QAAQ;YAYF,OAAO;IA4CrB,OAAO,CAAC,kBAAkB;IA8BpB,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhG,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIjI,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhI,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIlI,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAG1G"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { RateLimiter } from "./utils/rate-limiter.js";
|
|
2
|
+
export class SyncroApiError extends Error {
|
|
3
|
+
statusCode;
|
|
4
|
+
details;
|
|
5
|
+
constructor(message, statusCode, details) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.statusCode = statusCode;
|
|
8
|
+
this.details = details;
|
|
9
|
+
this.name = "SyncroApiError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class SyncroApiClient {
|
|
13
|
+
baseUrl;
|
|
14
|
+
apiKey;
|
|
15
|
+
rateLimiter;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
if (!config.apiKey)
|
|
18
|
+
throw new Error("SYNCRO_API_KEY is required");
|
|
19
|
+
if (!config.subdomain)
|
|
20
|
+
throw new Error("SYNCRO_SUBDOMAIN is required");
|
|
21
|
+
this.baseUrl = `https://${config.subdomain}.syncromsp.com/api/v1`;
|
|
22
|
+
this.apiKey = config.apiKey;
|
|
23
|
+
this.rateLimiter = new RateLimiter(180);
|
|
24
|
+
}
|
|
25
|
+
buildUrl(path, params) {
|
|
26
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
27
|
+
if (params) {
|
|
28
|
+
for (const [key, value] of Object.entries(params)) {
|
|
29
|
+
if (value !== undefined) {
|
|
30
|
+
url.searchParams.set(key, String(value));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return url.toString();
|
|
35
|
+
}
|
|
36
|
+
async request(method, path, options = {}) {
|
|
37
|
+
await this.rateLimiter.acquire();
|
|
38
|
+
const url = this.buildUrl(path, options.params);
|
|
39
|
+
const headers = {
|
|
40
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
41
|
+
Accept: "application/json",
|
|
42
|
+
};
|
|
43
|
+
const fetchOptions = { method, headers };
|
|
44
|
+
if (options.body) {
|
|
45
|
+
headers["Content-Type"] = "application/json";
|
|
46
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
47
|
+
}
|
|
48
|
+
const response = await fetch(url, fetchOptions);
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
let details;
|
|
51
|
+
try {
|
|
52
|
+
details = (await response.json());
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// response may not be JSON
|
|
56
|
+
}
|
|
57
|
+
const message = this.formatErrorMessage(response.status, details);
|
|
58
|
+
throw new SyncroApiError(message, response.status, details);
|
|
59
|
+
}
|
|
60
|
+
if (response.status === 204) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return (await response.json());
|
|
64
|
+
}
|
|
65
|
+
formatErrorMessage(status, details) {
|
|
66
|
+
const base = `Syncro API error (${status})`;
|
|
67
|
+
if (status === 401)
|
|
68
|
+
return `${base}: Invalid or expired API key. Check SYNCRO_API_KEY.`;
|
|
69
|
+
if (status === 403)
|
|
70
|
+
return `${base}: Insufficient permissions. Check your API token's custom permissions.`;
|
|
71
|
+
if (status === 404)
|
|
72
|
+
return `${base}: Resource not found.`;
|
|
73
|
+
if (status === 429)
|
|
74
|
+
return `${base}: Rate limit exceeded. Please wait before retrying.`;
|
|
75
|
+
if (details?.errors) {
|
|
76
|
+
let errMsg;
|
|
77
|
+
if (Array.isArray(details.errors)) {
|
|
78
|
+
errMsg = details.errors.join(", ");
|
|
79
|
+
}
|
|
80
|
+
else if (typeof details.errors === "string") {
|
|
81
|
+
errMsg = details.errors;
|
|
82
|
+
}
|
|
83
|
+
else if (typeof details.errors === "object") {
|
|
84
|
+
// Handle nested error objects like { mute_for: ["is not valid"] }
|
|
85
|
+
errMsg = Object.entries(details.errors)
|
|
86
|
+
.map(([field, msgs]) => `${field}: ${Array.isArray(msgs) ? msgs.join(", ") : msgs}`)
|
|
87
|
+
.join("; ");
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
errMsg = String(details.errors);
|
|
91
|
+
}
|
|
92
|
+
if (errMsg.length > 0)
|
|
93
|
+
return `${base}: ${errMsg}`;
|
|
94
|
+
}
|
|
95
|
+
if (details?.error)
|
|
96
|
+
return `${base}: ${details.error}`;
|
|
97
|
+
if (details?.message)
|
|
98
|
+
return `${base}: ${details.message}`;
|
|
99
|
+
return base;
|
|
100
|
+
}
|
|
101
|
+
async get(path, params) {
|
|
102
|
+
return this.request("GET", path, { params });
|
|
103
|
+
}
|
|
104
|
+
async post(path, body, params) {
|
|
105
|
+
return this.request("POST", path, { body, params });
|
|
106
|
+
}
|
|
107
|
+
async put(path, body, params) {
|
|
108
|
+
return this.request("PUT", path, { body, params });
|
|
109
|
+
}
|
|
110
|
+
async patch(path, body, params) {
|
|
111
|
+
return this.request("PATCH", path, { body, params });
|
|
112
|
+
}
|
|
113
|
+
async delete(path, params) {
|
|
114
|
+
return this.request("DELETE", path, { params });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=api-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { OAuthServerProvider, AuthorizationParams } from "@modelcontextprotocol/sdk/server/auth/provider.js";
|
|
2
|
+
import type { OAuthRegisteredClientsStore } from "@modelcontextprotocol/sdk/server/auth/clients.js";
|
|
3
|
+
import type { OAuthClientInformationFull, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
4
|
+
import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
|
|
5
|
+
import type { Response } from "express";
|
|
6
|
+
declare class InMemoryClientsStore implements OAuthRegisteredClientsStore {
|
|
7
|
+
private readonly clients;
|
|
8
|
+
getClient(clientId: string): Promise<OAuthClientInformationFull | undefined>;
|
|
9
|
+
registerClient(clientMetadata: OAuthClientInformationFull): Promise<OAuthClientInformationFull>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* OAuth provider for MCP server authentication.
|
|
13
|
+
*
|
|
14
|
+
* Implements the MCP OAuth 2.1 + PKCE spec so that Claude.ai and other
|
|
15
|
+
* remote MCP clients can authenticate against this server.
|
|
16
|
+
*
|
|
17
|
+
* The authorize flow auto-approves requests — since the server operator
|
|
18
|
+
* deployed this themselves with their Syncro API key, any client that
|
|
19
|
+
* can reach the server and complete the OAuth flow is authorized.
|
|
20
|
+
*/
|
|
21
|
+
export declare class McpOAuthProvider implements OAuthServerProvider {
|
|
22
|
+
readonly clientsStore: InMemoryClientsStore;
|
|
23
|
+
private readonly codes;
|
|
24
|
+
private readonly tokens;
|
|
25
|
+
private readonly tokenLifetimeMs;
|
|
26
|
+
constructor(tokenLifetimeHours?: number);
|
|
27
|
+
authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void>;
|
|
28
|
+
challengeForAuthorizationCode(_client: OAuthClientInformationFull, authorizationCode: string): Promise<string>;
|
|
29
|
+
exchangeAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string, _codeVerifier?: string): Promise<OAuthTokens>;
|
|
30
|
+
exchangeRefreshToken(client: OAuthClientInformationFull, refreshToken: string, _scopes?: string[], _resource?: URL): Promise<OAuthTokens>;
|
|
31
|
+
verifyAccessToken(token: string): Promise<AuthInfo>;
|
|
32
|
+
revokeToken(_client: OAuthClientInformationFull, request: {
|
|
33
|
+
token: string;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,mDAAmD,CAAC;AAC3D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,kDAAkD,CAAC;AACpG,OAAO,KAAK,EACV,0BAA0B,EAC1B,WAAW,EACZ,MAAM,0CAA0C,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gDAAgD,CAAC;AAC/E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAexC,cAAM,oBAAqB,YAAW,2BAA2B;IAC/D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiD;IAEnE,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,GAAG,SAAS,CAAC;IAI5E,cAAc,CAClB,cAAc,EAAE,0BAA0B,GACzC,OAAO,CAAC,0BAA0B,CAAC;CAIvC;AAED;;;;;;;;;GASG;AACH,qBAAa,gBAAiB,YAAW,mBAAmB;IAC1D,QAAQ,CAAC,YAAY,uBAA8B;IACnD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;gBAE7B,kBAAkB,GAAE,MAAW;IAIrC,SAAS,CACb,MAAM,EAAE,0BAA0B,EAClC,MAAM,EAAE,mBAAmB,EAC3B,GAAG,EAAE,QAAQ,GACZ,OAAO,CAAC,IAAI,CAAC;IAyBV,6BAA6B,CACjC,OAAO,EAAE,0BAA0B,EACnC,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,MAAM,CAAC;IAQZ,yBAAyB,CAC7B,MAAM,EAAE,0BAA0B,EAClC,iBAAiB,EAAE,MAAM,EACzB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,WAAW,CAAC;IAwCjB,oBAAoB,CACxB,MAAM,EAAE,0BAA0B,EAClC,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,MAAM,EAAE,EAClB,SAAS,CAAC,EAAE,GAAG,GACd,OAAO,CAAC,WAAW,CAAC;IA4BjB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAcnD,WAAW,CACf,OAAO,EAAE,0BAA0B,EACnC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GACzB,OAAO,CAAC,IAAI,CAAC;CAGjB"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
class InMemoryClientsStore {
|
|
3
|
+
clients = new Map();
|
|
4
|
+
async getClient(clientId) {
|
|
5
|
+
return this.clients.get(clientId);
|
|
6
|
+
}
|
|
7
|
+
async registerClient(clientMetadata) {
|
|
8
|
+
this.clients.set(clientMetadata.client_id, clientMetadata);
|
|
9
|
+
return clientMetadata;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* OAuth provider for MCP server authentication.
|
|
14
|
+
*
|
|
15
|
+
* Implements the MCP OAuth 2.1 + PKCE spec so that Claude.ai and other
|
|
16
|
+
* remote MCP clients can authenticate against this server.
|
|
17
|
+
*
|
|
18
|
+
* The authorize flow auto-approves requests — since the server operator
|
|
19
|
+
* deployed this themselves with their Syncro API key, any client that
|
|
20
|
+
* can reach the server and complete the OAuth flow is authorized.
|
|
21
|
+
*/
|
|
22
|
+
export class McpOAuthProvider {
|
|
23
|
+
clientsStore = new InMemoryClientsStore();
|
|
24
|
+
codes = new Map();
|
|
25
|
+
tokens = new Map();
|
|
26
|
+
tokenLifetimeMs;
|
|
27
|
+
constructor(tokenLifetimeHours = 24) {
|
|
28
|
+
this.tokenLifetimeMs = tokenLifetimeHours * 60 * 60 * 1000;
|
|
29
|
+
}
|
|
30
|
+
async authorize(client, params, res) {
|
|
31
|
+
// Auto-approve: the server operator already authorized usage by deploying
|
|
32
|
+
// with their Syncro API key. Generate an auth code and redirect back.
|
|
33
|
+
const code = randomUUID();
|
|
34
|
+
this.codes.set(code, { client, params });
|
|
35
|
+
// Clean up expired codes (older than 10 minutes)
|
|
36
|
+
const cutoff = Date.now() - 10 * 60 * 1000;
|
|
37
|
+
for (const [key, value] of this.codes) {
|
|
38
|
+
if (!this.tokens.has(key)) {
|
|
39
|
+
// We don't have a timestamp on codes, but they're short-lived
|
|
40
|
+
// and cleaned up when exchanged. This is best-effort cleanup.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const redirectUrl = new URL(params.redirectUri);
|
|
44
|
+
redirectUrl.searchParams.set("code", code);
|
|
45
|
+
if (params.state !== undefined) {
|
|
46
|
+
redirectUrl.searchParams.set("state", params.state);
|
|
47
|
+
}
|
|
48
|
+
res.redirect(redirectUrl.toString());
|
|
49
|
+
}
|
|
50
|
+
async challengeForAuthorizationCode(_client, authorizationCode) {
|
|
51
|
+
const codeData = this.codes.get(authorizationCode);
|
|
52
|
+
if (!codeData) {
|
|
53
|
+
throw new Error("Invalid authorization code");
|
|
54
|
+
}
|
|
55
|
+
return codeData.params.codeChallenge;
|
|
56
|
+
}
|
|
57
|
+
async exchangeAuthorizationCode(client, authorizationCode, _codeVerifier) {
|
|
58
|
+
const codeData = this.codes.get(authorizationCode);
|
|
59
|
+
if (!codeData) {
|
|
60
|
+
throw new Error("Invalid authorization code");
|
|
61
|
+
}
|
|
62
|
+
if (codeData.client.client_id !== client.client_id) {
|
|
63
|
+
throw new Error("Authorization code was not issued to this client");
|
|
64
|
+
}
|
|
65
|
+
this.codes.delete(authorizationCode);
|
|
66
|
+
const accessToken = randomUUID();
|
|
67
|
+
const refreshToken = randomUUID();
|
|
68
|
+
this.tokens.set(accessToken, {
|
|
69
|
+
token: accessToken,
|
|
70
|
+
clientId: client.client_id,
|
|
71
|
+
scopes: codeData.params.scopes || [],
|
|
72
|
+
expiresAt: Date.now() + this.tokenLifetimeMs,
|
|
73
|
+
resource: codeData.params.resource,
|
|
74
|
+
});
|
|
75
|
+
// Store refresh token with longer lifetime (30 days)
|
|
76
|
+
this.tokens.set(refreshToken, {
|
|
77
|
+
token: refreshToken,
|
|
78
|
+
clientId: client.client_id,
|
|
79
|
+
scopes: codeData.params.scopes || [],
|
|
80
|
+
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
|
|
81
|
+
resource: codeData.params.resource,
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
access_token: accessToken,
|
|
85
|
+
token_type: "bearer",
|
|
86
|
+
expires_in: Math.floor(this.tokenLifetimeMs / 1000),
|
|
87
|
+
refresh_token: refreshToken,
|
|
88
|
+
scope: (codeData.params.scopes || []).join(" "),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async exchangeRefreshToken(client, refreshToken, _scopes, _resource) {
|
|
92
|
+
const tokenData = this.tokens.get(refreshToken);
|
|
93
|
+
if (!tokenData || tokenData.expiresAt < Date.now()) {
|
|
94
|
+
throw new Error("Invalid or expired refresh token");
|
|
95
|
+
}
|
|
96
|
+
if (tokenData.clientId !== client.client_id) {
|
|
97
|
+
throw new Error("Refresh token was not issued to this client");
|
|
98
|
+
}
|
|
99
|
+
// Issue new access token
|
|
100
|
+
const newAccessToken = randomUUID();
|
|
101
|
+
this.tokens.set(newAccessToken, {
|
|
102
|
+
token: newAccessToken,
|
|
103
|
+
clientId: client.client_id,
|
|
104
|
+
scopes: tokenData.scopes,
|
|
105
|
+
expiresAt: Date.now() + this.tokenLifetimeMs,
|
|
106
|
+
resource: tokenData.resource,
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
access_token: newAccessToken,
|
|
110
|
+
token_type: "bearer",
|
|
111
|
+
expires_in: Math.floor(this.tokenLifetimeMs / 1000),
|
|
112
|
+
refresh_token: refreshToken,
|
|
113
|
+
scope: tokenData.scopes.join(" "),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async verifyAccessToken(token) {
|
|
117
|
+
const tokenData = this.tokens.get(token);
|
|
118
|
+
if (!tokenData || tokenData.expiresAt < Date.now()) {
|
|
119
|
+
throw new Error("Invalid or expired token");
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
token,
|
|
123
|
+
clientId: tokenData.clientId,
|
|
124
|
+
scopes: tokenData.scopes,
|
|
125
|
+
expiresAt: Math.floor(tokenData.expiresAt / 1000),
|
|
126
|
+
resource: tokenData.resource,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async revokeToken(_client, request) {
|
|
130
|
+
this.tokens.delete(request.token);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../../src/domains/admin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,aAAa,CAAC;AAI7D,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,aAAa,CAgyBnE"}
|