superops-it 1.1.18 → 2.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.
- package/README.md +23 -65
- package/build/examples.json +197 -0
- package/build/http-server.js +236 -0
- package/build/index.js +28 -0
- package/build/mcp/tools/api.js +82 -0
- package/build/mcp/tools/apiSchema.js +173 -0
- package/build/mcp/tools/index.js +2 -0
- package/build/server.js +22 -0
- package/build/utils/clientConfig.js +38 -0
- package/build/utils/graphqlClient.js +113 -0
- package/build/utils/introspection.js +197 -0
- package/build/utils/logger.js +29 -0
- package/build/utils/queryBuilder.js +86 -0
- package/build/utils/responseFormatter.js +25 -0
- package/package.json +21 -18
- package/docs/api-index.json +0 -7857
- package/src/client.mjs +0 -159
- package/src/index.mjs +0 -367
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SuperOps IT Teams MCP Server
|
|
2
2
|
|
|
3
|
-
An MCP
|
|
3
|
+
An MCP server that gives AI assistants full access to the SuperOps IT Teams GraphQL API through two dynamic tools powered by live schema introspection.
|
|
4
4
|
|
|
5
5
|
## What is SuperOps IT Teams?
|
|
6
6
|
|
|
@@ -12,8 +12,6 @@ SuperOps IT Teams is for **internal IT departments** - IT teams within a single
|
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
|
-
We recommend using [bun](https://bun.sh) for faster startup times - MCP servers start on every request, so speed matters.
|
|
16
|
-
|
|
17
15
|
```bash
|
|
18
16
|
bunx superops-it@latest
|
|
19
17
|
```
|
|
@@ -24,16 +22,6 @@ Or with npx:
|
|
|
24
22
|
npx superops-it@latest
|
|
25
23
|
```
|
|
26
24
|
|
|
27
|
-
Or install globally:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
bun install -g superops-it@latest
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
npm install -g superops-it@latest
|
|
35
|
-
```
|
|
36
|
-
|
|
37
25
|
## Configuration
|
|
38
26
|
|
|
39
27
|
### Claude Code
|
|
@@ -76,82 +64,52 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
76
64
|
|
|
77
65
|
Get your API key from **SuperOps Admin > API Settings**.
|
|
78
66
|
|
|
79
|
-
##
|
|
67
|
+
## Tools
|
|
80
68
|
|
|
81
|
-
### `
|
|
69
|
+
### `superops-api`
|
|
82
70
|
|
|
83
|
-
|
|
71
|
+
Execute any SuperOps API operation by name. The server builds the GraphQL query automatically from the introspected schema.
|
|
84
72
|
|
|
85
73
|
```
|
|
86
|
-
|
|
74
|
+
superops-api({ operation: "getTicketList", params: { page: 1, pageSize: 10 } })
|
|
75
|
+
superops-api({ operation: "getAsset", params: { assetId: "12345" } })
|
|
76
|
+
superops-api({ operation: "getPriorityList" })
|
|
87
77
|
```
|
|
88
78
|
|
|
89
|
-
### `
|
|
79
|
+
### `superops-api-schema`
|
|
90
80
|
|
|
91
|
-
|
|
81
|
+
Discover available operations and their parameter schemas.
|
|
92
82
|
|
|
93
83
|
```
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
superops-api-schema() → Category summary
|
|
85
|
+
superops-api-schema({ category: "Ticket" }) → Operations in category
|
|
86
|
+
superops-api-schema({ operation: "getTicket" }) → Full parameter details
|
|
87
|
+
superops-api-schema({ examples: true }) → Usage patterns and gotchas
|
|
88
|
+
superops-api-schema({ operation: "getTicket", examples: true }) → Schema + example
|
|
96
89
|
```
|
|
97
90
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
Get full details of a type definition.
|
|
101
|
-
|
|
102
|
-
```
|
|
103
|
-
get_superops_type({ name: "Ticket" })
|
|
104
|
-
get_superops_type({ name: "Department" })
|
|
105
|
-
```
|
|
91
|
+
## Transport
|
|
106
92
|
|
|
107
|
-
|
|
93
|
+
| Mode | Flag | Use Case |
|
|
94
|
+
|------|------|----------|
|
|
95
|
+
| stdio | *(default)* | Claude Code, Claude Desktop |
|
|
96
|
+
| Streamable HTTP | `--http` | Web clients, containers |
|
|
97
|
+
| Legacy SSE | `--sse` | Older MCP clients |
|
|
108
98
|
|
|
109
|
-
|
|
99
|
+
## API Limits
|
|
110
100
|
|
|
111
|
-
```
|
|
112
|
-
list_superops_operations({ type: "queries" })
|
|
113
|
-
list_superops_operations({ type: "mutations" })
|
|
114
|
-
list_superops_operations({ type: "all" })
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### `execute_graphql`
|
|
118
|
-
|
|
119
|
-
Execute a GraphQL query or mutation against the SuperOps API. Requires environment variables (see [Configuration](#configuration)).
|
|
120
|
-
|
|
121
|
-
```
|
|
122
|
-
execute_graphql({
|
|
123
|
-
operation: "query { getTicket(id: \"123\") { id subject status } }"
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
execute_graphql({
|
|
127
|
-
operation: "mutation createTicket($input: CreateTicketInput!) { createTicket(input: $input) { id } }",
|
|
128
|
-
variables: { input: { subject: "New ticket", departmentId: "456" } }
|
|
129
|
-
})
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**API limits and notes:**
|
|
133
101
|
- Maximum 800 API requests per minute
|
|
134
|
-
- Date/time values must be in UTC
|
|
135
|
-
- Use `null` to clear/reset attribute values
|
|
102
|
+
- Date/time values must be in UTC with ISO format (e.g., `2026-03-17T10:15:30`)
|
|
136
103
|
|
|
137
104
|
## API Endpoints
|
|
138
105
|
|
|
139
106
|
- **US**: `https://api.superops.ai/it`
|
|
140
107
|
- **EU**: `https://euapi.superops.ai/it`
|
|
141
108
|
|
|
142
|
-
## Example Usage
|
|
143
|
-
|
|
144
|
-
Once configured, ask Claude:
|
|
145
|
-
|
|
146
|
-
- "How do I create a ticket in SuperOps?"
|
|
147
|
-
- "What fields are available on the Asset type?"
|
|
148
|
-
- "Search for department-related operations"
|
|
149
|
-
- "Show me how to update a user"
|
|
150
|
-
|
|
151
109
|
## Related
|
|
152
110
|
|
|
153
111
|
- [superops-msp](https://npmjs.com/package/superops-msp) - For SuperOps MSP (managed service providers)
|
|
154
112
|
|
|
155
113
|
## License
|
|
156
114
|
|
|
157
|
-
|
|
115
|
+
MIT
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_note": "Usage examples for tricky API patterns. Loaded by superops-api-schema when examples are requested.",
|
|
3
|
+
|
|
4
|
+
"getTicket": {
|
|
5
|
+
"description": "Fetch a single ticket by its ID",
|
|
6
|
+
"params": { "ticketId": "25507674456317952" },
|
|
7
|
+
"notes": "The field is `ticketId` (not `id`). Get ticket IDs from getTicketList."
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
"getTicketList": {
|
|
11
|
+
"description": "List tickets with optional filtering and pagination",
|
|
12
|
+
"params": { "page": 1, "pageSize": 25 },
|
|
13
|
+
"notes": "Returns `{ tickets: [...], listInfo: { totalCount, hasMore } }`. Max pageSize is 100.",
|
|
14
|
+
"filtering": {
|
|
15
|
+
"simple_filter": {
|
|
16
|
+
"description": "Filter by a single field",
|
|
17
|
+
"params": {
|
|
18
|
+
"page": 1,
|
|
19
|
+
"pageSize": 25,
|
|
20
|
+
"conditions": [
|
|
21
|
+
{ "fieldName": "statusId", "operator": "is", "value": "Open" }
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"compound_filter": {
|
|
26
|
+
"description": "Combine multiple conditions with AND/OR",
|
|
27
|
+
"params": {
|
|
28
|
+
"page": 1,
|
|
29
|
+
"pageSize": 25,
|
|
30
|
+
"conditions": [
|
|
31
|
+
{
|
|
32
|
+
"joinOperator": "OR",
|
|
33
|
+
"operands": [
|
|
34
|
+
{ "fieldName": "priorityName", "operator": "is", "value": "High" },
|
|
35
|
+
{ "fieldName": "priorityName", "operator": "is", "value": "Urgent" }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"notes": "Use `joinOperator` + `operands` for compound conditions, NOT `operator` + `value` at the top level."
|
|
41
|
+
},
|
|
42
|
+
"date_filter": {
|
|
43
|
+
"description": "Filter by date range",
|
|
44
|
+
"params": {
|
|
45
|
+
"page": 1,
|
|
46
|
+
"pageSize": 25,
|
|
47
|
+
"conditions": [
|
|
48
|
+
{ "fieldName": "createdTime", "operator": "after", "value": "2026-03-01T00:00:00Z" }
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"notes": "Date operators: `after`, `before`, `between`. NOT `greater than` or `less than`."
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
"createTicket": {
|
|
57
|
+
"description": "Create a new ticket",
|
|
58
|
+
"params": {
|
|
59
|
+
"subject": "Printer not working",
|
|
60
|
+
"description": "The printer on 3rd floor is showing error code E-301",
|
|
61
|
+
"requestType": "Incident",
|
|
62
|
+
"clientId": "12345",
|
|
63
|
+
"priority": { "name": "Medium" }
|
|
64
|
+
},
|
|
65
|
+
"notes": "`requestType` is required — use 'Incident' (default), 'Service Request', or 'Change Request'. `clientId` is required."
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
"updateTicket": {
|
|
69
|
+
"description": "Update an existing ticket",
|
|
70
|
+
"params": {
|
|
71
|
+
"ticketId": "25507674456317952",
|
|
72
|
+
"status": { "name": "In Progress" },
|
|
73
|
+
"priority": { "name": "High" }
|
|
74
|
+
},
|
|
75
|
+
"notes": "Only include the fields you want to change."
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
"createTicketConversation": {
|
|
79
|
+
"description": "Reply to a ticket",
|
|
80
|
+
"params": {
|
|
81
|
+
"ticketId": "25507674456317952",
|
|
82
|
+
"content": "<p>We're looking into this issue.</p>",
|
|
83
|
+
"conversationType": "REPLY"
|
|
84
|
+
},
|
|
85
|
+
"notes": "Content must be HTML. conversationType: REPLY (to requester), FORWARD, or NOTE."
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
"createTicketNote": {
|
|
89
|
+
"description": "Add a private note to a ticket",
|
|
90
|
+
"params": {
|
|
91
|
+
"ticketId": "25507674456317952",
|
|
92
|
+
"content": "<p>Internal note: checked the logs</p>",
|
|
93
|
+
"privacyType": "PRIVATE"
|
|
94
|
+
},
|
|
95
|
+
"notes": "privacyType: PRIVATE (technicians only) or PUBLIC (visible to requester)."
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
"getAsset": {
|
|
99
|
+
"description": "Fetch a single asset",
|
|
100
|
+
"params": { "assetId": "12345" },
|
|
101
|
+
"notes": "Field is `assetId`, not `id`."
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
"getAssetList": {
|
|
105
|
+
"description": "List assets with pagination",
|
|
106
|
+
"params": { "page": 1, "pageSize": 25 },
|
|
107
|
+
"notes": "Same pagination and filtering pattern as getTicketList."
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
"getClient": {
|
|
111
|
+
"description": "Fetch a single client",
|
|
112
|
+
"params": { "accountId": "12345" },
|
|
113
|
+
"notes": "Field is `accountId`, not `id` or `clientId`."
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
"getClientList": {
|
|
117
|
+
"description": "List clients",
|
|
118
|
+
"params": { "page": 1, "pageSize": 25 },
|
|
119
|
+
"notes": "Same pagination pattern as getTicketList."
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
"createClientV2": {
|
|
123
|
+
"description": "Create a new client (use V2, not createClient)",
|
|
124
|
+
"params": {
|
|
125
|
+
"name": "Acme Corp",
|
|
126
|
+
"hqSite": {
|
|
127
|
+
"name": "HQ",
|
|
128
|
+
"timeZone": "America/New_York"
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"notes": "Use `createClientV2` (not `createClient`). `hqSite` with `name` and `timeZone` is required."
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
"getClientUserList": {
|
|
135
|
+
"description": "List contacts for a client",
|
|
136
|
+
"params": { "clientId": "12345", "page": 1, "pageSize": 25 },
|
|
137
|
+
"notes": "The `site` field on contacts is often null. Use `getClientUserAssociationList` to get user-site mappings."
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
"resolveAlerts": {
|
|
141
|
+
"description": "Resolve one or more alerts",
|
|
142
|
+
"params": { "alertIds": ["alert-id-1", "alert-id-2"] },
|
|
143
|
+
"notes": "Accepts an array of alert IDs to bulk-resolve."
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
"runScriptOnAsset": {
|
|
147
|
+
"description": "Execute a script on an asset",
|
|
148
|
+
"params": {
|
|
149
|
+
"scriptId": "12345",
|
|
150
|
+
"assetId": "67890"
|
|
151
|
+
},
|
|
152
|
+
"notes": "Script output (stdout/stderr) is NOT available via this API. Check the SuperOps UI for results."
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
"createKbArticle": {
|
|
156
|
+
"description": "Create a knowledge base article",
|
|
157
|
+
"params": {
|
|
158
|
+
"title": "How to reset password",
|
|
159
|
+
"content": "<p>Step 1: Go to settings...</p>",
|
|
160
|
+
"status": "PUBLISHED",
|
|
161
|
+
"portalType": "REQUESTER",
|
|
162
|
+
"clientSharedType": "ALL_CLIENTS",
|
|
163
|
+
"userSharedType": "ALL_USERS"
|
|
164
|
+
},
|
|
165
|
+
"notes": "For TECHNICIAN portal, you must also set `groupSharedType`. Visibility fields: portalType (REQUESTER or TECHNICIAN), clientSharedType (ALL_CLIENTS or specific), userSharedType (ALL_USERS or specific)."
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
"createWorklogEntries": {
|
|
169
|
+
"description": "Log time against a ticket",
|
|
170
|
+
"params": {
|
|
171
|
+
"ticketId": "25507674456317952",
|
|
172
|
+
"entries": [{
|
|
173
|
+
"qty": "1.5",
|
|
174
|
+
"billDateTime": "2026-03-16T10:00:00Z",
|
|
175
|
+
"description": "Troubleshooting printer issue",
|
|
176
|
+
"billable": true
|
|
177
|
+
}]
|
|
178
|
+
},
|
|
179
|
+
"notes": "`qty` is decimal hours as a string. `billDateTime` is ISO 8601."
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
"_pagination": {
|
|
183
|
+
"description": "General pagination pattern used by all list operations",
|
|
184
|
+
"params": { "page": 1, "pageSize": 25 },
|
|
185
|
+
"notes": "Most list operations accept `page` (1-indexed) and `pageSize` (max 100). Response includes `listInfo: { totalCount, hasMore }`."
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
"_filtering": {
|
|
189
|
+
"description": "General filtering pattern for list operations",
|
|
190
|
+
"notes": "Conditions use `{ fieldName, operator, value }`. Compound conditions use `{ joinOperator: 'AND'|'OR', operands: [...conditions] }`. Date operators are `after`, `before`, `between` (NOT `greater than`). Sort with `{ fieldName, order: 'ASC'|'DESC' }`."
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
"_id_fields": {
|
|
194
|
+
"description": "ID field naming conventions — a common gotcha",
|
|
195
|
+
"notes": "Ticket: `ticketId`. Asset: `assetId`. Client: `accountId`. Client User: `userId`. Site: `siteId`. Script: `scriptId`. Alert: `alertId`. These are NOT just `id`."
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import express from "express";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createServer } from "./server.js";
|
|
8
|
+
import { createLogger } from "./utils/logger.js";
|
|
9
|
+
const PORT = parseInt(process.env.MCP_PORT || "3000", 10);
|
|
10
|
+
const logger = createLogger("MCP-HTTP-Server");
|
|
11
|
+
export async function main() {
|
|
12
|
+
const app = express();
|
|
13
|
+
app.use(express.json());
|
|
14
|
+
const transports = {
|
|
15
|
+
streamable: {},
|
|
16
|
+
sse: {},
|
|
17
|
+
};
|
|
18
|
+
// Health check
|
|
19
|
+
app.get("/health", (_req, res) => {
|
|
20
|
+
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
21
|
+
});
|
|
22
|
+
// Modern Streamable HTTP (POST /mcp)
|
|
23
|
+
app.post("/mcp", async (req, res) => {
|
|
24
|
+
try {
|
|
25
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
26
|
+
let transport;
|
|
27
|
+
if (sessionId && transports.streamable[sessionId]) {
|
|
28
|
+
transport = transports.streamable[sessionId];
|
|
29
|
+
}
|
|
30
|
+
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
31
|
+
transport = new StreamableHTTPServerTransport({
|
|
32
|
+
sessionIdGenerator: () => randomUUID(),
|
|
33
|
+
onsessioninitialized: (sid) => {
|
|
34
|
+
transports.streamable[sid] = transport;
|
|
35
|
+
logger.info("New MCP session initialized", { sessionId: sid });
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
transport.onclose = () => {
|
|
39
|
+
if (transport.sessionId) {
|
|
40
|
+
logger.info("MCP session closed", {
|
|
41
|
+
sessionId: transport.sessionId,
|
|
42
|
+
});
|
|
43
|
+
delete transports.streamable[transport.sessionId];
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const server = createServer();
|
|
47
|
+
await server.connect(transport);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
res.status(400).json({
|
|
51
|
+
jsonrpc: "2.0",
|
|
52
|
+
error: {
|
|
53
|
+
code: -32000,
|
|
54
|
+
message: "Bad Request: Invalid session or initialization",
|
|
55
|
+
},
|
|
56
|
+
id: null,
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await transport.handleRequest(req, res, req.body);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
logger.error("Error handling HTTP request", {
|
|
64
|
+
error: error instanceof Error ? error.message : String(error),
|
|
65
|
+
});
|
|
66
|
+
if (!res.headersSent) {
|
|
67
|
+
res.status(500).json({
|
|
68
|
+
jsonrpc: "2.0",
|
|
69
|
+
error: { code: -32603, message: "Internal server error" },
|
|
70
|
+
id: null,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// Streamable HTTP GET (server-to-client notifications)
|
|
76
|
+
const handleSessionRequest = async (req, res) => {
|
|
77
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
78
|
+
if (!sessionId || !transports.streamable[sessionId]) {
|
|
79
|
+
res.status(400).json({
|
|
80
|
+
jsonrpc: "2.0",
|
|
81
|
+
error: { code: -32000, message: "Invalid or missing session ID" },
|
|
82
|
+
id: null,
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await transports.streamable[sessionId].handleRequest(req, res);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
logger.error("Error handling session request", {
|
|
91
|
+
error: error instanceof Error ? error.message : String(error),
|
|
92
|
+
sessionId,
|
|
93
|
+
});
|
|
94
|
+
if (!res.headersSent) {
|
|
95
|
+
res.status(500).json({
|
|
96
|
+
jsonrpc: "2.0",
|
|
97
|
+
error: { code: -32603, message: "Internal server error" },
|
|
98
|
+
id: null,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
app.get("/mcp", handleSessionRequest);
|
|
104
|
+
// Streamable HTTP DELETE (session termination)
|
|
105
|
+
app.delete("/mcp", async (req, res) => {
|
|
106
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
107
|
+
if (!sessionId || !transports.streamable[sessionId]) {
|
|
108
|
+
res.status(400).json({
|
|
109
|
+
jsonrpc: "2.0",
|
|
110
|
+
error: { code: -32000, message: "Invalid or missing session ID" },
|
|
111
|
+
id: null,
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await transports.streamable[sessionId].handleRequest(req, res);
|
|
117
|
+
if (transports.streamable[sessionId]) {
|
|
118
|
+
logger.info("MCP session terminated", { sessionId });
|
|
119
|
+
delete transports.streamable[sessionId];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
logger.error("Error handling DELETE request", {
|
|
124
|
+
error: error instanceof Error ? error.message : String(error),
|
|
125
|
+
sessionId,
|
|
126
|
+
});
|
|
127
|
+
if (!res.headersSent) {
|
|
128
|
+
res.status(500).json({
|
|
129
|
+
jsonrpc: "2.0",
|
|
130
|
+
error: { code: -32603, message: "Internal server error" },
|
|
131
|
+
id: null,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// Legacy SSE endpoint
|
|
137
|
+
app.get("/sse", async (_req, res) => {
|
|
138
|
+
try {
|
|
139
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
140
|
+
transports.sse[transport.sessionId] = transport;
|
|
141
|
+
res.on("close", () => {
|
|
142
|
+
logger.info("Legacy SSE session closed", {
|
|
143
|
+
sessionId: transport.sessionId,
|
|
144
|
+
});
|
|
145
|
+
delete transports.sse[transport.sessionId];
|
|
146
|
+
});
|
|
147
|
+
const server = createServer();
|
|
148
|
+
await server.connect(transport);
|
|
149
|
+
logger.info("New legacy SSE session initialized", {
|
|
150
|
+
sessionId: transport.sessionId,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
logger.error("Error handling SSE request", {
|
|
155
|
+
error: error instanceof Error ? error.message : String(error),
|
|
156
|
+
});
|
|
157
|
+
if (!res.headersSent) {
|
|
158
|
+
res.status(500).json({
|
|
159
|
+
jsonrpc: "2.0",
|
|
160
|
+
error: { code: -32603, message: "Internal server error" },
|
|
161
|
+
id: null,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
// Legacy message endpoint
|
|
167
|
+
app.post("/messages", async (req, res) => {
|
|
168
|
+
try {
|
|
169
|
+
const sessionId = req.query.sessionId;
|
|
170
|
+
if (!sessionId) {
|
|
171
|
+
res.status(400).json({
|
|
172
|
+
jsonrpc: "2.0",
|
|
173
|
+
error: {
|
|
174
|
+
code: -32000,
|
|
175
|
+
message: "sessionId query parameter is required",
|
|
176
|
+
},
|
|
177
|
+
id: null,
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const transport = transports.sse[sessionId];
|
|
182
|
+
if (!transport) {
|
|
183
|
+
res.status(400).json({
|
|
184
|
+
jsonrpc: "2.0",
|
|
185
|
+
error: {
|
|
186
|
+
code: -32000,
|
|
187
|
+
message: "No transport found for sessionId",
|
|
188
|
+
},
|
|
189
|
+
id: null,
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
await transport.handlePostMessage(req, res, req.body);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
logger.error("Error handling legacy message", {
|
|
197
|
+
error: error instanceof Error ? error.message : String(error),
|
|
198
|
+
});
|
|
199
|
+
if (!res.headersSent) {
|
|
200
|
+
res.status(500).json({
|
|
201
|
+
jsonrpc: "2.0",
|
|
202
|
+
error: { code: -32603, message: "Internal server error" },
|
|
203
|
+
id: null,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
app.listen(PORT, () => {
|
|
209
|
+
logger.info("SuperOps MSP MCP server started", {
|
|
210
|
+
port: PORT,
|
|
211
|
+
protocols: [
|
|
212
|
+
"Streamable HTTP (MCP 2025-03-26)",
|
|
213
|
+
"Legacy SSE (MCP 2024-11-05)",
|
|
214
|
+
],
|
|
215
|
+
endpoints: {
|
|
216
|
+
modern: {
|
|
217
|
+
mcp: `http://localhost:${PORT}/mcp`,
|
|
218
|
+
health: `http://localhost:${PORT}/health`,
|
|
219
|
+
},
|
|
220
|
+
legacy: {
|
|
221
|
+
sse: `http://localhost:${PORT}/sse`,
|
|
222
|
+
messages: `http://localhost:${PORT}/messages`,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
229
|
+
main().catch((error) => {
|
|
230
|
+
logger.error("Fatal error", {
|
|
231
|
+
error: error instanceof Error ? error.message : String(error),
|
|
232
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
233
|
+
});
|
|
234
|
+
process.exit(1);
|
|
235
|
+
});
|
|
236
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createServer } from "./server.js";
|
|
4
|
+
import { createLogger } from "./utils/logger.js";
|
|
5
|
+
const logger = createLogger("MCP-Entry");
|
|
6
|
+
async function main() {
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const httpMode = args.includes("--http") ||
|
|
9
|
+
args.includes("--sse") ||
|
|
10
|
+
process.env.MCP_TRANSPORT === "http" ||
|
|
11
|
+
process.env.MCP_TRANSPORT === "sse";
|
|
12
|
+
if (httpMode) {
|
|
13
|
+
const httpModule = await import("./http-server.js");
|
|
14
|
+
return httpModule.main();
|
|
15
|
+
}
|
|
16
|
+
// Default: stdio mode
|
|
17
|
+
const server = createServer();
|
|
18
|
+
const transport = new StdioServerTransport();
|
|
19
|
+
await server.connect(transport);
|
|
20
|
+
logger.info("SuperOps MSP MCP server running via stdio");
|
|
21
|
+
}
|
|
22
|
+
main().catch((error) => {
|
|
23
|
+
logger.error("Fatal error", {
|
|
24
|
+
error: error instanceof Error ? error.message : String(error),
|
|
25
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
26
|
+
});
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { createLogger } from "../../utils/logger.js";
|
|
3
|
+
import { ResponseFormatter } from "../../utils/responseFormatter.js";
|
|
4
|
+
import { executeGraphQL, SuperOpsAPIError, } from "../../utils/graphqlClient.js";
|
|
5
|
+
import { getSchemaCache } from "../../utils/introspection.js";
|
|
6
|
+
import { buildQuery, wrapVariables } from "../../utils/queryBuilder.js";
|
|
7
|
+
const logger = createLogger("SuperOpsApi");
|
|
8
|
+
export const inputSchema = z.object({
|
|
9
|
+
operation: z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.describe('The operation name, e.g. "getTicket", "createClient", "resolveAlerts"'),
|
|
13
|
+
params: z
|
|
14
|
+
.record(z.string(), z.unknown())
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Parameters object — passed as GraphQL variables."),
|
|
17
|
+
});
|
|
18
|
+
// outputSchema omitted — responses are dynamic (different shape per operation).
|
|
19
|
+
// structuredContent is still returned for hosts that support it.
|
|
20
|
+
export const name = "superops-api";
|
|
21
|
+
export let description = "Execute any SuperOps API operation. Use superops-api-schema to discover available operations and their parameters.";
|
|
22
|
+
let descriptionInitialized = false;
|
|
23
|
+
async function ensureDescription() {
|
|
24
|
+
if (descriptionInitialized)
|
|
25
|
+
return;
|
|
26
|
+
try {
|
|
27
|
+
const cache = await getSchemaCache();
|
|
28
|
+
description = `Execute any SuperOps API operation. Use superops-api-schema to discover available operations and their parameters.\n\nAvailable operations (${cache.operations.size} total):\n${cache.operationsList}`;
|
|
29
|
+
descriptionInitialized = true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
logger.warn("Could not fetch operations list for description enrichment");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const annotations = {
|
|
36
|
+
title: "SuperOps API",
|
|
37
|
+
readOnlyHint: false,
|
|
38
|
+
openWorldHint: true,
|
|
39
|
+
};
|
|
40
|
+
export async function handler(input) {
|
|
41
|
+
await ensureDescription();
|
|
42
|
+
const { operation, params = {} } = input;
|
|
43
|
+
logger.info(`Executing: ${operation}`, {
|
|
44
|
+
hasParams: Object.keys(params).length > 0,
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
const cache = await getSchemaCache();
|
|
48
|
+
const op = cache.operations.get(operation);
|
|
49
|
+
if (!op) {
|
|
50
|
+
const allNames = [...cache.operations.keys()];
|
|
51
|
+
const suggestions = allNames
|
|
52
|
+
.filter((n) => n.toLowerCase().includes(operation.toLowerCase()))
|
|
53
|
+
.slice(0, 5);
|
|
54
|
+
const hint = suggestions.length > 0
|
|
55
|
+
? `\nDid you mean: ${suggestions.join(", ")}?`
|
|
56
|
+
: "\nUse superops-api-schema to list available operations.";
|
|
57
|
+
return ResponseFormatter.error(`Unknown operation: "${operation}"`, hint);
|
|
58
|
+
}
|
|
59
|
+
const { query } = await buildQuery(operation);
|
|
60
|
+
const variables = await wrapVariables(operation, params);
|
|
61
|
+
const data = await executeGraphQL(query, variables);
|
|
62
|
+
return ResponseFormatter.success(`${operation} succeeded`, data);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
logger.error(`${operation} failed`, {
|
|
66
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
67
|
+
});
|
|
68
|
+
if (error instanceof SuperOpsAPIError) {
|
|
69
|
+
if (error.isAuthError()) {
|
|
70
|
+
return ResponseFormatter.error("Authentication failed", "Check your SUPEROPS_API_KEY and SUPEROPS_SUBDOMAIN configuration.");
|
|
71
|
+
}
|
|
72
|
+
if (error.isRateLimited()) {
|
|
73
|
+
return ResponseFormatter.error("Rate limited", "Too many requests. Please try again shortly.");
|
|
74
|
+
}
|
|
75
|
+
if (error.isGraphQLError()) {
|
|
76
|
+
return ResponseFormatter.error(`GraphQL error for ${operation}`, error.message);
|
|
77
|
+
}
|
|
78
|
+
return ResponseFormatter.error(`API error for ${operation}`, error.message);
|
|
79
|
+
}
|
|
80
|
+
return ResponseFormatter.error(`Failed: ${operation}`, `${error instanceof Error ? error.message : "Unknown error"}`);
|
|
81
|
+
}
|
|
82
|
+
}
|