superops-it 1.1.16 → 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/src/client.mjs DELETED
@@ -1,159 +0,0 @@
1
- /**
2
- * SuperOps IT Teams GraphQL Client
3
- *
4
- * Features:
5
- * - Bearer token authentication
6
- * - Configurable timeout with AbortController
7
- * - Exponential backoff retry for transient failures
8
- * - Read-only mode to block mutations
9
- * - Handles GraphQL errors returned with 200 status
10
- */
11
-
12
- const DEFAULT_TIMEOUT_MS = 30000;
13
- const MAX_RETRIES = 3;
14
- const RETRY_DELAYS = [1000, 2000, 4000];
15
-
16
- /**
17
- * Structured error for SuperOps API failures
18
- */
19
- export class SuperOpsAPIError extends Error {
20
- constructor(status, body, context = {}) {
21
- const message = SuperOpsAPIError.formatMessage(status, body);
22
- super(message);
23
-
24
- this.name = 'SuperOpsAPIError';
25
- this.status = status;
26
- this.body = body;
27
- this.context = context;
28
- }
29
-
30
- static formatMessage(status, body) {
31
- if (status === 200 && Array.isArray(body)) {
32
- // Try to extract message fields first
33
- const messages = body.map(e => e.message).filter(Boolean);
34
- if (messages.length > 0) {
35
- return messages.join('; ');
36
- }
37
- // SuperOps sometimes returns null message with details in extensions.clientError
38
- const clientErrors = body.flatMap(e =>
39
- e.extensions?.clientError?.map(ce =>
40
- `${ce.code}: ${ce.param?.attributes?.join(', ') || 'unknown'}`
41
- ) || []
42
- );
43
- if (clientErrors.length > 0) {
44
- return clientErrors.join('; ');
45
- }
46
- // Fallback to full error structure
47
- return JSON.stringify(body, null, 2);
48
- }
49
- return `HTTP ${status}: ${body?.message || body?.error || 'Request failed'}`;
50
- }
51
-
52
- isRateLimited() { return this.status === 429; }
53
- isAuthError() { return this.status === 401 || this.status === 403; }
54
- isServerError() { return this.status >= 500; }
55
- isGraphQLError() { return this.status === 200 && Array.isArray(this.body); }
56
- isRetryable() { return this.isRateLimited() || this.isServerError(); }
57
- }
58
-
59
- function sleep(ms) {
60
- return new Promise(resolve => setTimeout(resolve, ms));
61
- }
62
-
63
- async function withRetry(fn, maxRetries = MAX_RETRIES) {
64
- let lastError;
65
-
66
- for (let attempt = 0; attempt < maxRetries; attempt++) {
67
- try {
68
- return await fn();
69
- } catch (error) {
70
- lastError = error;
71
-
72
- if (!(error instanceof SuperOpsAPIError) || !error.isRetryable()) {
73
- throw error;
74
- }
75
-
76
- if (attempt < maxRetries - 1) {
77
- await sleep(RETRY_DELAYS[attempt]);
78
- }
79
- }
80
- }
81
-
82
- throw lastError;
83
- }
84
-
85
- export class SuperOpsClient {
86
- constructor(config) {
87
- if (!config.apiKey) {
88
- throw new Error('SUPEROPS_API_KEY is required');
89
- }
90
- if (!config.subdomain) {
91
- throw new Error('SUPEROPS_SUBDOMAIN is required');
92
- }
93
-
94
- this.apiKey = config.apiKey;
95
- this.subdomain = config.subdomain;
96
- this.region = config.region || 'us';
97
- this.timeout = config.timeout || DEFAULT_TIMEOUT_MS;
98
- this.readOnly = config.readOnly ?? false;
99
-
100
- const host = this.region === 'eu' ? 'euapi.superops.ai' : 'api.superops.ai';
101
- this.endpoint = `https://${host}/it`;
102
- }
103
-
104
- async execute(operation, variables = {}) {
105
- if (this.readOnly && operation.trim().toLowerCase().startsWith('mutation')) {
106
- throw new Error(
107
- 'Mutations are disabled in read-only mode. ' +
108
- 'Set SUPEROPS_READ_ONLY=false to enable mutations.'
109
- );
110
- }
111
-
112
- const context = { operation, variables, endpoint: this.endpoint };
113
-
114
- return withRetry(async () => {
115
- const controller = new AbortController();
116
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
117
-
118
- try {
119
- const response = await fetch(this.endpoint, {
120
- method: 'POST',
121
- headers: {
122
- 'Content-Type': 'application/json',
123
- 'Authorization': `Bearer ${this.apiKey}`,
124
- 'CustomerSubDomain': this.subdomain,
125
- 'User-Agent': 'superops-it-mcp/1.0'
126
- },
127
- body: JSON.stringify({ query: operation, variables }),
128
- signal: controller.signal
129
- });
130
-
131
- const rawText = await response.text();
132
-
133
- let body;
134
- try {
135
- body = JSON.parse(rawText);
136
- } catch (e) {
137
- throw new Error(`Invalid JSON response (HTTP ${response.status}): ${rawText.substring(0, 200)}`);
138
- }
139
-
140
- if (!response.ok) {
141
- throw new SuperOpsAPIError(response.status, body, context);
142
- }
143
-
144
- if (body.errors?.length) {
145
- throw new SuperOpsAPIError(200, body.errors, context);
146
- }
147
-
148
- return body.data;
149
- } catch (error) {
150
- if (error.name === 'AbortError') {
151
- throw new Error(`Request timed out after ${this.timeout}ms`);
152
- }
153
- throw error;
154
- } finally {
155
- clearTimeout(timeoutId);
156
- }
157
- });
158
- }
159
- }
package/src/index.mjs DELETED
@@ -1,367 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import {
5
- CallToolRequestSchema,
6
- ListToolsRequestSchema
7
- } from '@modelcontextprotocol/sdk/types.js';
8
- import { readFile } from 'fs/promises';
9
- import { fileURLToPath } from 'url';
10
- import { dirname, join } from 'path';
11
- import { createRequire } from 'module';
12
- import { SuperOpsClient, SuperOpsAPIError } from './client.mjs';
13
-
14
- const __dirname = dirname(fileURLToPath(import.meta.url));
15
- const require = createRequire(import.meta.url);
16
- const pkg = require('../package.json');
17
-
18
- const SERVER_NAME = 'superops-it';
19
- const SERVER_VERSION = pkg.version;
20
- const PRODUCT_NAME = 'SuperOps IT Teams';
21
-
22
- // API data cache
23
- let apiData = null;
24
-
25
- // API client (lazy initialization)
26
- let client = null;
27
-
28
- function getClient() {
29
- if (!client && process.env.SUPEROPS_API_KEY && process.env.SUPEROPS_SUBDOMAIN) {
30
- client = new SuperOpsClient({
31
- apiKey: process.env.SUPEROPS_API_KEY,
32
- subdomain: process.env.SUPEROPS_SUBDOMAIN,
33
- region: process.env.SUPEROPS_REGION,
34
- timeout: parseInt(process.env.SUPEROPS_TIMEOUT) || undefined,
35
- readOnly: process.env.SUPEROPS_READ_ONLY === 'true'
36
- });
37
- }
38
- return client;
39
- }
40
-
41
- async function loadApiData() {
42
- if (apiData) return apiData;
43
-
44
- try {
45
- const indexPath = join(__dirname, '..', 'docs', 'api-index.json');
46
- const content = await readFile(indexPath, 'utf-8');
47
- apiData = JSON.parse(content);
48
- return apiData;
49
- } catch (error) {
50
- throw new Error(`Failed to load API data: ${error.message}`);
51
- }
52
- }
53
-
54
- // Create server
55
- const server = new Server(
56
- { name: SERVER_NAME, version: SERVER_VERSION },
57
- {
58
- instructions: 'Use this server when the user needs help with the SuperOps IT Teams GraphQL API for internal IT departments. Provides documentation for tickets, assets, departments, users, and internal service desk operations. Search for API queries, mutations, and type definitions.',
59
- capabilities: { tools: {} }
60
- }
61
- );
62
-
63
- // List available tools
64
- server.setRequestHandler(ListToolsRequestSchema, async () => {
65
- return {
66
- tools: [
67
- {
68
- name: 'search_superops_api',
69
- description: `Search the ${PRODUCT_NAME} API documentation for queries, mutations, and types`,
70
- inputSchema: {
71
- type: 'object',
72
- properties: {
73
- query: {
74
- type: 'string',
75
- description: 'Search term to find in operation names, descriptions, and types'
76
- }
77
- },
78
- required: ['query']
79
- }
80
- },
81
- {
82
- name: 'get_superops_operation',
83
- description: `Get full details of a specific ${PRODUCT_NAME} API query or mutation`,
84
- inputSchema: {
85
- type: 'object',
86
- properties: {
87
- name: {
88
- type: 'string',
89
- description: 'Name of the query or mutation (e.g., "getTicket", "getAsset")'
90
- }
91
- },
92
- required: ['name']
93
- }
94
- },
95
- {
96
- name: 'get_superops_type',
97
- description: `Get full details of a ${PRODUCT_NAME} API type definition`,
98
- inputSchema: {
99
- type: 'object',
100
- properties: {
101
- name: {
102
- type: 'string',
103
- description: 'Name of the type (e.g., "Ticket", "Asset", "Department")'
104
- }
105
- },
106
- required: ['name']
107
- }
108
- },
109
- {
110
- name: 'list_superops_operations',
111
- description: `List all available ${PRODUCT_NAME} API operations`,
112
- inputSchema: {
113
- type: 'object',
114
- properties: {
115
- type: {
116
- type: 'string',
117
- enum: ['queries', 'mutations', 'all'],
118
- description: 'Type of operations to list'
119
- }
120
- },
121
- required: ['type']
122
- }
123
- },
124
- {
125
- name: 'execute_graphql',
126
- description: `Execute a GraphQL query or mutation against the ${PRODUCT_NAME} API. Requires SUPEROPS_API_KEY environment variable.
127
-
128
- IMPORTANT: Before constructing queries, you MUST look up the correct field names and syntax:
129
- 1. Call get_superops_operation to get the query template and see the input type name
130
- 2. Call get_superops_type for the input type (e.g., ListInfoInput) to see exact field names (pageSize not limit)
131
- 3. Call get_superops_type for any nested types (e.g., SortInput, SortOrder) to see enum values (use DESC not "desc")
132
- 4. Call get_superops_type for the return type (e.g., Ticket, Client) to see available fields (accountId not id)
133
-
134
- Do NOT guess field names - they are non-standard. Always look them up first.`,
135
- inputSchema: {
136
- type: 'object',
137
- properties: {
138
- operation: {
139
- type: 'string',
140
- description: 'The GraphQL query or mutation to execute'
141
- },
142
- variables: {
143
- type: 'object',
144
- description: 'Variables for the operation (optional)'
145
- }
146
- },
147
- required: ['operation']
148
- }
149
- }
150
- ]
151
- };
152
- });
153
-
154
- // Handle tool calls
155
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
156
- const { name, arguments: args } = request.params;
157
-
158
- try {
159
- const data = await loadApiData();
160
-
161
- switch (name) {
162
- case 'search_superops_api': {
163
- const query = args.query.toLowerCase();
164
- const results = {
165
- queries: [],
166
- mutations: [],
167
- types: []
168
- };
169
-
170
- for (const op of data.queries) {
171
- if (op.name.toLowerCase().includes(query) ||
172
- op.description?.toLowerCase().includes(query)) {
173
- results.queries.push({
174
- name: op.name,
175
- description: op.description,
176
- returns: op.returns
177
- });
178
- }
179
- }
180
-
181
- for (const op of data.mutations) {
182
- if (op.name.toLowerCase().includes(query) ||
183
- op.description?.toLowerCase().includes(query)) {
184
- results.mutations.push({
185
- name: op.name,
186
- description: op.description,
187
- returns: op.returns
188
- });
189
- }
190
- }
191
-
192
- for (const type of data.types) {
193
- if (type.name.toLowerCase().includes(query) ||
194
- type.description?.toLowerCase().includes(query)) {
195
- results.types.push({
196
- name: type.name,
197
- kind: type.kind,
198
- description: type.description
199
- });
200
- }
201
- }
202
-
203
- const total = results.queries.length + results.mutations.length + results.types.length;
204
- return {
205
- content: [{
206
- type: 'text',
207
- text: JSON.stringify({
208
- searchTerm: args.query,
209
- totalResults: total,
210
- results
211
- }, null, 2)
212
- }]
213
- };
214
- }
215
-
216
- case 'get_superops_operation': {
217
- const opName = args.name.toLowerCase();
218
-
219
- let operation = data.queries.find(q => q.name.toLowerCase() === opName);
220
- if (!operation) {
221
- operation = data.mutations.find(m => m.name.toLowerCase() === opName);
222
- }
223
-
224
- if (!operation) {
225
- return {
226
- content: [{
227
- type: 'text',
228
- text: `Operation "${args.name}" not found. Use search_superops_api to find available operations.`
229
- }],
230
- isError: true
231
- };
232
- }
233
-
234
- return {
235
- content: [{
236
- type: 'text',
237
- text: JSON.stringify(operation, null, 2)
238
- }]
239
- };
240
- }
241
-
242
- case 'get_superops_type': {
243
- const typeName = args.name.toLowerCase();
244
- const type = data.types.find(t => t.name.toLowerCase() === typeName);
245
-
246
- if (!type) {
247
- return {
248
- content: [{
249
- type: 'text',
250
- text: `Type "${args.name}" not found. Use search_superops_api to find available types.`
251
- }],
252
- isError: true
253
- };
254
- }
255
-
256
- return {
257
- content: [{
258
- type: 'text',
259
- text: JSON.stringify(type, null, 2)
260
- }]
261
- };
262
- }
263
-
264
- case 'list_superops_operations': {
265
- const listType = args.type;
266
- let result = {};
267
-
268
- if (listType === 'queries' || listType === 'all') {
269
- result.queries = data.queries.map(q => ({
270
- name: q.name,
271
- description: q.description
272
- }));
273
- }
274
-
275
- if (listType === 'mutations' || listType === 'all') {
276
- result.mutations = data.mutations.map(m => ({
277
- name: m.name,
278
- description: m.description
279
- }));
280
- }
281
-
282
- result.meta = data.meta;
283
-
284
- return {
285
- content: [{
286
- type: 'text',
287
- text: JSON.stringify(result, null, 2)
288
- }]
289
- };
290
- }
291
-
292
- case 'execute_graphql': {
293
- const apiClient = getClient();
294
-
295
- if (!apiClient) {
296
- return {
297
- content: [{
298
- type: 'text',
299
- text: 'API execution requires SUPEROPS_API_KEY and SUPEROPS_SUBDOMAIN environment variables.\n\nGet your API key from SuperOps Admin > API Settings.\nYour subdomain is the prefix in your SuperOps URL (e.g., "acme" from acme.superops.ai).'
300
- }],
301
- isError: true
302
- };
303
- }
304
-
305
- try {
306
- const result = await apiClient.execute(
307
- args.operation,
308
- args.variables || {}
309
- );
310
-
311
- return {
312
- content: [{
313
- type: 'text',
314
- text: JSON.stringify(result, null, 2)
315
- }]
316
- };
317
- } catch (error) {
318
- let message;
319
-
320
- if (error instanceof SuperOpsAPIError) {
321
- if (error.isAuthError()) {
322
- message = 'Authentication failed. Check your SUPEROPS_API_KEY.';
323
- } else if (error.isRateLimited()) {
324
- message = 'Rate limited after retries. Try again later.';
325
- } else if (error.isGraphQLError()) {
326
- message = `GraphQL Error: ${error.message}`;
327
- } else {
328
- message = `API Error (${error.status}): ${error.message}`;
329
- }
330
- } else {
331
- message = error.message;
332
- }
333
-
334
- return {
335
- content: [{ type: 'text', text: message }],
336
- isError: true
337
- };
338
- }
339
- }
340
-
341
- default:
342
- return {
343
- content: [{
344
- type: 'text',
345
- text: `Unknown tool: ${name}`
346
- }],
347
- isError: true
348
- };
349
- }
350
- } catch (error) {
351
- return {
352
- content: [{
353
- type: 'text',
354
- text: `Error: ${error.message}`
355
- }],
356
- isError: true
357
- };
358
- }
359
- });
360
-
361
- // Start server
362
- async function main() {
363
- const transport = new StdioServerTransport();
364
- await server.connect(transport);
365
- }
366
-
367
- main().catch(console.error);