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.
@@ -0,0 +1,173 @@
1
+ import * as z from "zod/v4";
2
+ import { readFile } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, join } from "node:path";
5
+ import { createLogger } from "../../utils/logger.js";
6
+ import { ResponseFormatter } from "../../utils/responseFormatter.js";
7
+ import { getSchemaCache } from "../../utils/introspection.js";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ let cachedExamples = null;
11
+ async function loadExamples() {
12
+ if (cachedExamples)
13
+ return cachedExamples;
14
+ const examplesPath = join(__dirname, "..", "..", "examples.json");
15
+ const raw = await readFile(examplesPath, "utf-8");
16
+ cachedExamples = JSON.parse(raw);
17
+ return cachedExamples;
18
+ }
19
+ const logger = createLogger("SuperOpsApiSchema");
20
+ export const inputSchema = z.object({
21
+ category: z
22
+ .string()
23
+ .optional()
24
+ .describe('Filter by category, e.g. "Ticket", "Asset", "Client"'),
25
+ operation: z
26
+ .string()
27
+ .optional()
28
+ .describe('Get full details for a specific operation, e.g. "getTicket"'),
29
+ examples: z
30
+ .boolean()
31
+ .optional()
32
+ .describe("Include usage examples with the response. Works with operation (specific example) or alone (all examples + general patterns)."),
33
+ });
34
+ // outputSchema omitted — responses vary by query mode (categories, operation details, examples).
35
+ // structuredContent is still returned for hosts that support it.
36
+ export const name = "superops-api-schema";
37
+ export const description = "Discover available SuperOps API operations and their parameter schemas. " +
38
+ "Call with no params for a category summary, with category to list operations, " +
39
+ "with operation for full parameter details, or with examples:true for usage patterns and gotchas.";
40
+ export const annotations = {
41
+ title: "SuperOps API Schema",
42
+ readOnlyHint: true,
43
+ idempotentHint: true,
44
+ };
45
+ export async function handler(input) {
46
+ try {
47
+ // Examples-only mode (no operation specified) — doesn't need API
48
+ if (input.examples && !input.operation && !input.category) {
49
+ const allExamples = await loadExamples();
50
+ return ResponseFormatter.success("Usage examples and common patterns", allExamples);
51
+ }
52
+ const cache = await getSchemaCache();
53
+ // Specific operation requested
54
+ if (input.operation) {
55
+ const op = cache.operations.get(input.operation);
56
+ if (!op) {
57
+ const suggestions = [...cache.operations.keys()]
58
+ .filter((n) => n.toLowerCase().includes(input.operation.toLowerCase()))
59
+ .slice(0, 5);
60
+ const hint = suggestions.length > 0
61
+ ? `Did you mean: ${suggestions.join(", ")}?`
62
+ : "Use without params to see available categories.";
63
+ return ResponseFormatter.error(`Operation not found: "${input.operation}"`, hint);
64
+ }
65
+ const argDetails = op.args.map((arg) => {
66
+ const inputType = cache.types.get(arg.typeName);
67
+ return {
68
+ name: arg.name,
69
+ type: arg.typeName,
70
+ required: arg.isNonNull,
71
+ isList: arg.isList,
72
+ description: arg.description,
73
+ ...(inputType?.inputFields && {
74
+ fields: inputType.inputFields.map((f) => ({
75
+ name: f.name,
76
+ type: f.typeName,
77
+ required: f.isNonNull,
78
+ isList: f.isList,
79
+ description: f.description,
80
+ ...resolveInputFields(f.typeName, cache.types),
81
+ })),
82
+ }),
83
+ };
84
+ });
85
+ const returnType = cache.types.get(op.returnTypeName);
86
+ const returnFields = returnType?.fields?.map((f) => ({
87
+ name: f.name,
88
+ type: f.typeName,
89
+ isList: f.isList,
90
+ description: f.description,
91
+ }));
92
+ // Include usage example if requested and available
93
+ let example = undefined;
94
+ if (input.examples) {
95
+ const allExamples = await loadExamples();
96
+ example = allExamples[input.operation] || null;
97
+ }
98
+ return ResponseFormatter.success(`Schema for ${input.operation}`, {
99
+ name: op.name,
100
+ type: op.type,
101
+ description: op.description,
102
+ arguments: argDetails,
103
+ returnType: op.returnTypeName,
104
+ returnFields,
105
+ ...(example !== undefined && { example }),
106
+ });
107
+ }
108
+ // Category filter
109
+ if (input.category) {
110
+ const matchedCategory = [...cache.categories.keys()].find((c) => c.toLowerCase() === input.category.toLowerCase());
111
+ if (!matchedCategory) {
112
+ const available = [...cache.categories.keys()].sort().join(", ");
113
+ return ResponseFormatter.error(`Category not found: "${input.category}"`, `Available categories: ${available}`);
114
+ }
115
+ const opNames = cache.categories.get(matchedCategory) || [];
116
+ const ops = opNames.map((opName) => {
117
+ const op = cache.operations.get(opName);
118
+ return {
119
+ name: opName,
120
+ type: op?.type,
121
+ description: op?.description,
122
+ args: op?.args
123
+ .map((a) => `${a.name}: ${a.typeName}${a.isNonNull ? "!" : ""}`)
124
+ .join(", ") || "none",
125
+ };
126
+ });
127
+ const queries = ops.filter((o) => o.type === "query");
128
+ const mutations = ops.filter((o) => o.type === "mutation");
129
+ return ResponseFormatter.success(`Operations in ${matchedCategory}`, {
130
+ category: matchedCategory,
131
+ totalOperations: ops.length,
132
+ queries,
133
+ mutations,
134
+ });
135
+ }
136
+ // No filter — category summary
137
+ const summary = {};
138
+ for (const [category, opNames] of cache.categories) {
139
+ const queries = opNames.filter((n) => cache.operations.get(n)?.type === "query").length;
140
+ const mutations = opNames.filter((n) => cache.operations.get(n)?.type === "mutation").length;
141
+ summary[category] = {
142
+ total: opNames.length,
143
+ queries,
144
+ mutations,
145
+ operations: opNames,
146
+ };
147
+ }
148
+ return ResponseFormatter.success("Available API categories", {
149
+ totalOperations: cache.operations.size,
150
+ totalCategories: cache.categories.size,
151
+ categories: summary,
152
+ });
153
+ }
154
+ catch (error) {
155
+ logger.error("Failed to fetch API schema", {
156
+ error: error instanceof Error ? error.message : "Unknown error",
157
+ });
158
+ return ResponseFormatter.error("Failed to fetch API schema", `Could not retrieve schema: ${error instanceof Error ? error.message : "Unknown error"}`);
159
+ }
160
+ }
161
+ function resolveInputFields(typeName, types) {
162
+ const typeInfo = types.get(typeName);
163
+ if (!typeInfo?.inputFields)
164
+ return {};
165
+ return {
166
+ fields: typeInfo.inputFields.map((f) => ({
167
+ name: f.name,
168
+ type: f.typeName,
169
+ required: f.isNonNull,
170
+ description: f.description,
171
+ })),
172
+ };
173
+ }
@@ -0,0 +1,2 @@
1
+ export * as api from "./api.js";
2
+ export * as apiSchema from "./apiSchema.js";
@@ -0,0 +1,22 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as api from "./mcp/tools/api.js";
3
+ import * as apiSchema from "./mcp/tools/apiSchema.js";
4
+ export function createServer() {
5
+ const server = new McpServer({
6
+ name: "superops-it",
7
+ version: "2.0.0",
8
+ });
9
+ server.registerTool(api.name, {
10
+ title: api.annotations.title,
11
+ description: api.description,
12
+ inputSchema: api.inputSchema,
13
+ annotations: api.annotations,
14
+ }, api.handler);
15
+ server.registerTool(apiSchema.name, {
16
+ title: apiSchema.annotations.title,
17
+ description: apiSchema.description,
18
+ inputSchema: apiSchema.inputSchema,
19
+ annotations: apiSchema.annotations,
20
+ }, apiSchema.handler);
21
+ return server;
22
+ }
@@ -0,0 +1,38 @@
1
+ class ConfigManager {
2
+ static instance;
3
+ config = null;
4
+ constructor() { }
5
+ static getInstance() {
6
+ if (!ConfigManager.instance) {
7
+ ConfigManager.instance = new ConfigManager();
8
+ }
9
+ return ConfigManager.instance;
10
+ }
11
+ getConfig() {
12
+ if (!this.config) {
13
+ this.config = this.loadConfig();
14
+ }
15
+ return this.config;
16
+ }
17
+ loadConfig() {
18
+ const apiKey = process.env.SUPEROPS_API_KEY;
19
+ const subdomain = process.env.SUPEROPS_SUBDOMAIN;
20
+ if (!apiKey)
21
+ throw new Error("SUPEROPS_API_KEY is required");
22
+ if (!subdomain)
23
+ throw new Error("SUPEROPS_SUBDOMAIN is required");
24
+ const region = (process.env.SUPEROPS_REGION || "us");
25
+ const host = region === "eu" ? "euapi.superops.ai" : "api.superops.ai";
26
+ return {
27
+ apiKey,
28
+ subdomain,
29
+ region,
30
+ timeout: parseInt(process.env.SUPEROPS_TIMEOUT || "30000", 10),
31
+ readOnly: process.env.SUPEROPS_READ_ONLY === "true",
32
+ endpoint: `https://${host}/it`,
33
+ };
34
+ }
35
+ }
36
+ export function getClientConfig() {
37
+ return ConfigManager.getInstance().getConfig();
38
+ }
@@ -0,0 +1,113 @@
1
+ import { getClientConfig } from "./clientConfig.js";
2
+ import { createLogger } from "./logger.js";
3
+ const logger = createLogger("GraphQLClient");
4
+ const MAX_RETRIES = 3;
5
+ const RETRY_DELAYS = [1000, 2000, 4000];
6
+ export class SuperOpsAPIError extends Error {
7
+ status;
8
+ body;
9
+ context;
10
+ constructor(status, body, context = {}) {
11
+ const message = SuperOpsAPIError.formatMessage(status, body);
12
+ super(message);
13
+ this.name = "SuperOpsAPIError";
14
+ this.status = status;
15
+ this.body = body;
16
+ this.context = context;
17
+ }
18
+ static formatMessage(status, body) {
19
+ if (status === 200 && Array.isArray(body)) {
20
+ const messages = body
21
+ .map((e) => e.message)
22
+ .filter(Boolean);
23
+ if (messages.length > 0)
24
+ return messages.join("; ");
25
+ const clientErrors = body.flatMap((e) => {
26
+ const ext = e.extensions;
27
+ const errs = ext?.clientError;
28
+ return errs?.map((ce) => `${ce.code}: ${ce.param?.attributes?.join(", ") || "unknown"}`) || [];
29
+ });
30
+ if (clientErrors.length > 0)
31
+ return clientErrors.join("; ");
32
+ return JSON.stringify(body, null, 2);
33
+ }
34
+ const b = body;
35
+ return `HTTP ${status}: ${b?.message || b?.error || "Request failed"}`;
36
+ }
37
+ isRateLimited() { return this.status === 429; }
38
+ isAuthError() { return this.status === 401 || this.status === 403; }
39
+ isServerError() { return this.status >= 500; }
40
+ isGraphQLError() { return this.status === 200 && Array.isArray(this.body); }
41
+ isRetryable() { return this.isRateLimited() || this.isServerError(); }
42
+ }
43
+ function sleep(ms) {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+ async function withRetry(fn, maxRetries = MAX_RETRIES) {
47
+ let lastError;
48
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
49
+ try {
50
+ return await fn();
51
+ }
52
+ catch (error) {
53
+ lastError = error;
54
+ if (!(error instanceof SuperOpsAPIError) || !error.isRetryable())
55
+ throw error;
56
+ if (attempt < maxRetries - 1)
57
+ await sleep(RETRY_DELAYS[attempt]);
58
+ }
59
+ }
60
+ throw lastError;
61
+ }
62
+ export async function executeGraphQL(query, variables = {}) {
63
+ const config = getClientConfig();
64
+ if (config.readOnly && query.trim().toLowerCase().startsWith("mutation")) {
65
+ throw new Error("Mutations are disabled in read-only mode. Set SUPEROPS_READ_ONLY=false to enable.");
66
+ }
67
+ const context = { query: query.substring(0, 100), variables, endpoint: config.endpoint };
68
+ return withRetry(async () => {
69
+ const controller = new AbortController();
70
+ const timeoutId = setTimeout(() => controller.abort(), config.timeout);
71
+ try {
72
+ logger.debug("Executing GraphQL", {
73
+ endpoint: config.endpoint,
74
+ queryPrefix: query.substring(0, 60),
75
+ });
76
+ const response = await fetch(config.endpoint, {
77
+ method: "POST",
78
+ headers: {
79
+ "Content-Type": "application/json",
80
+ Authorization: `Bearer ${config.apiKey}`,
81
+ CustomerSubDomain: config.subdomain,
82
+ "User-Agent": "superops-it-mcp/2.0",
83
+ },
84
+ body: JSON.stringify({ query, variables }),
85
+ signal: controller.signal,
86
+ });
87
+ const rawText = await response.text();
88
+ let body;
89
+ try {
90
+ body = JSON.parse(rawText);
91
+ }
92
+ catch {
93
+ throw new Error(`Invalid JSON response (HTTP ${response.status}): ${rawText.substring(0, 200)}`);
94
+ }
95
+ if (!response.ok) {
96
+ throw new SuperOpsAPIError(response.status, body, context);
97
+ }
98
+ if (Array.isArray(body.errors) && body.errors.length > 0) {
99
+ throw new SuperOpsAPIError(200, body.errors, context);
100
+ }
101
+ return body.data;
102
+ }
103
+ catch (error) {
104
+ if (error instanceof Error && error.name === "AbortError") {
105
+ throw new Error(`Request timed out after ${config.timeout}ms`);
106
+ }
107
+ throw error;
108
+ }
109
+ finally {
110
+ clearTimeout(timeoutId);
111
+ }
112
+ });
113
+ }
@@ -0,0 +1,197 @@
1
+ import { executeGraphQL } from "./graphqlClient.js";
2
+ import { createLogger } from "./logger.js";
3
+ const logger = createLogger("Introspection");
4
+ // --- Singleton cache ---
5
+ let cache = null;
6
+ export async function getSchemaCache() {
7
+ if (cache)
8
+ return cache;
9
+ cache = await buildSchemaCache();
10
+ return cache;
11
+ }
12
+ // --- Introspection query ---
13
+ const INTROSPECTION_QUERY = `{
14
+ __schema {
15
+ queryType { name }
16
+ mutationType { name }
17
+ types {
18
+ name
19
+ kind
20
+ description
21
+ fields(includeDeprecated: false) {
22
+ name
23
+ description
24
+ type { ...TypeRef }
25
+ args {
26
+ name
27
+ description
28
+ type { ...TypeRef }
29
+ }
30
+ }
31
+ inputFields {
32
+ name
33
+ description
34
+ type { ...TypeRef }
35
+ }
36
+ enumValues(includeDeprecated: false) {
37
+ name
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ fragment TypeRef on __Type {
44
+ name
45
+ kind
46
+ ofType {
47
+ name
48
+ kind
49
+ ofType {
50
+ name
51
+ kind
52
+ ofType {
53
+ name
54
+ kind
55
+ ofType {
56
+ name
57
+ kind
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }`;
63
+ function unwrapType(typeRef) {
64
+ let isList = false;
65
+ let isNonNull = false;
66
+ let current = typeRef;
67
+ while (current.kind === "NON_NULL" || current.kind === "LIST") {
68
+ if (current.kind === "NON_NULL")
69
+ isNonNull = true;
70
+ if (current.kind === "LIST")
71
+ isList = true;
72
+ if (!current.ofType)
73
+ break;
74
+ current = current.ofType;
75
+ }
76
+ return {
77
+ typeName: current.name || "Unknown",
78
+ kind: current.kind,
79
+ isList,
80
+ isNonNull,
81
+ };
82
+ }
83
+ // --- Schema building ---
84
+ async function buildSchemaCache() {
85
+ logger.info("Running introspection query...");
86
+ const startTime = Date.now();
87
+ const data = (await executeGraphQL(INTROSPECTION_QUERY));
88
+ const schema = data.__schema;
89
+ const types = new Map();
90
+ const operations = new Map();
91
+ // Build type map
92
+ for (const rawType of schema.types) {
93
+ if (rawType.name.startsWith("__"))
94
+ continue;
95
+ const typeInfo = {
96
+ name: rawType.name,
97
+ kind: rawType.kind,
98
+ description: rawType.description || undefined,
99
+ };
100
+ if (rawType.fields) {
101
+ typeInfo.fields = rawType.fields.map((f) => ({
102
+ name: f.name,
103
+ ...unwrapType(f.type),
104
+ description: f.description || undefined,
105
+ }));
106
+ }
107
+ if (rawType.inputFields) {
108
+ typeInfo.inputFields = rawType.inputFields.map((f) => ({
109
+ name: f.name,
110
+ ...unwrapType(f.type),
111
+ description: f.description || undefined,
112
+ }));
113
+ }
114
+ if (rawType.enumValues) {
115
+ typeInfo.enumValues = rawType.enumValues.map((v) => v.name);
116
+ }
117
+ types.set(rawType.name, typeInfo);
118
+ }
119
+ // Extract operations from Query and Mutation root types
120
+ function extractOps(rootTypeName, opType) {
121
+ const rawType = schema.types.find((t) => t.name === rootTypeName);
122
+ if (!rawType?.fields)
123
+ return;
124
+ for (const rawField of rawType.fields) {
125
+ const returnInfo = unwrapType(rawField.type);
126
+ const args = (rawField.args || []).map((a) => ({
127
+ name: a.name,
128
+ ...unwrapType(a.type),
129
+ description: a.description || undefined,
130
+ }));
131
+ operations.set(rawField.name, {
132
+ name: rawField.name,
133
+ type: opType,
134
+ description: rawField.description || "",
135
+ args,
136
+ returnTypeName: returnInfo.typeName,
137
+ });
138
+ }
139
+ }
140
+ extractOps(schema.queryType.name, "query");
141
+ extractOps(schema.mutationType.name, "mutation");
142
+ // Derive categories
143
+ const categories = deriveCategories(operations);
144
+ // Build operations list string
145
+ const operationsList = buildOperationsList(categories, operations);
146
+ const elapsed = Date.now() - startTime;
147
+ logger.info(`Introspection complete: ${operations.size} operations, ${types.size} types in ${elapsed}ms`);
148
+ return { operations, types, categories, operationsList };
149
+ }
150
+ // --- Category derivation ---
151
+ const VERB_PREFIXES = [
152
+ "hardDelete",
153
+ "softDelete",
154
+ "get",
155
+ "create",
156
+ "update",
157
+ "delete",
158
+ "restore",
159
+ "run",
160
+ "resolve",
161
+ "assign",
162
+ "send",
163
+ "search",
164
+ "analyze",
165
+ "register",
166
+ "reset",
167
+ ];
168
+ function stripVerb(name) {
169
+ for (const prefix of VERB_PREFIXES) {
170
+ if (name.startsWith(prefix) && name.length > prefix.length) {
171
+ return name.substring(prefix.length);
172
+ }
173
+ }
174
+ return name;
175
+ }
176
+ function deriveCategories(operations) {
177
+ const categories = new Map();
178
+ for (const [name] of operations) {
179
+ const noun = stripVerb(name);
180
+ // Normalize: "TicketList" → "Ticket", trailing "V2"/"V3" → removed
181
+ const normalized = noun.replace(/List$/, "").replace(/V[23]$/, "").replace(/s$/, "") || name;
182
+ const existing = categories.get(normalized) || [];
183
+ existing.push(name);
184
+ categories.set(normalized, existing);
185
+ }
186
+ return categories;
187
+ }
188
+ function buildOperationsList(categories, operations) {
189
+ const lines = [];
190
+ const sorted = [...categories.entries()].sort((a, b) => a[0].localeCompare(b[0]));
191
+ for (const [category, opNames] of sorted) {
192
+ const queries = opNames.filter((n) => operations.get(n)?.type === "query").length;
193
+ const mutations = opNames.filter((n) => operations.get(n)?.type === "mutation").length;
194
+ lines.push(`${category}: ${queries}q/${mutations}m — ${opNames.join(", ")}`);
195
+ }
196
+ return lines.join("\n");
197
+ }
@@ -0,0 +1,29 @@
1
+ export var LogLevel;
2
+ (function (LogLevel) {
3
+ LogLevel["ERROR"] = "error";
4
+ LogLevel["WARN"] = "warn";
5
+ LogLevel["INFO"] = "info";
6
+ LogLevel["DEBUG"] = "debug";
7
+ })(LogLevel || (LogLevel = {}));
8
+ class Logger {
9
+ context;
10
+ constructor(context = "SuperOps-MCP") {
11
+ this.context = context;
12
+ }
13
+ log(level, message, meta) {
14
+ const entry = {
15
+ level,
16
+ message,
17
+ timestamp: new Date().toISOString(),
18
+ context: { service: this.context, ...meta },
19
+ };
20
+ console.error(JSON.stringify(entry));
21
+ }
22
+ error(message, meta) { this.log(LogLevel.ERROR, message, meta); }
23
+ warn(message, meta) { this.log(LogLevel.WARN, message, meta); }
24
+ info(message, meta) { this.log(LogLevel.INFO, message, meta); }
25
+ debug(message, meta) { this.log(LogLevel.DEBUG, message, meta); }
26
+ }
27
+ export function createLogger(context) {
28
+ return new Logger(context);
29
+ }
@@ -0,0 +1,86 @@
1
+ import { getSchemaCache } from "./introspection.js";
2
+ const MAX_DEPTH = 4;
3
+ export async function buildQuery(operationName) {
4
+ const cache = await getSchemaCache();
5
+ const op = cache.operations.get(operationName);
6
+ if (!op) {
7
+ throw new Error(`Unknown operation: "${operationName}"`);
8
+ }
9
+ const varDeclarations = [];
10
+ const argPassthroughs = [];
11
+ const variableNames = [];
12
+ for (const arg of op.args) {
13
+ const typeStr = formatGraphQLType(arg.typeName, arg.isNonNull, arg.isList);
14
+ varDeclarations.push(`$${arg.name}: ${typeStr}`);
15
+ argPassthroughs.push(`${arg.name}: $${arg.name}`);
16
+ variableNames.push(arg.name);
17
+ }
18
+ const varBlock = varDeclarations.length > 0 ? `(${varDeclarations.join(", ")})` : "";
19
+ const argBlock = argPassthroughs.length > 0 ? `(${argPassthroughs.join(", ")})` : "";
20
+ const selectionSet = buildSelectionSet(op.returnTypeName, cache.types, 0, new Set());
21
+ const keyword = op.type;
22
+ const query = `${keyword} ${operationName}${varBlock} {\n ${operationName}${argBlock}${selectionSet}\n}`;
23
+ return { query, variableNames };
24
+ }
25
+ function formatGraphQLType(typeName, isNonNull, isList) {
26
+ let result = typeName;
27
+ if (isList)
28
+ result = `[${result}]`;
29
+ if (isNonNull)
30
+ result = `${result}!`;
31
+ return result;
32
+ }
33
+ function buildSelectionSet(typeName, types, depth, visited) {
34
+ if (depth >= MAX_DEPTH)
35
+ return "";
36
+ const typeInfo = types.get(typeName);
37
+ if (!typeInfo)
38
+ return "";
39
+ if (typeInfo.kind === "SCALAR" || typeInfo.kind === "ENUM")
40
+ return "";
41
+ if (visited.has(typeName))
42
+ return "";
43
+ visited.add(typeName);
44
+ const fields = typeInfo.fields;
45
+ if (!fields || fields.length === 0)
46
+ return "";
47
+ const selections = [];
48
+ const indent = " ".repeat(depth + 2);
49
+ for (const field of fields) {
50
+ const nested = buildSelectionSet(field.typeName, types, depth + 1, new Set(visited));
51
+ if (nested) {
52
+ selections.push(`${indent}${field.name}${nested}`);
53
+ }
54
+ else if (isLeafType(field.kind, field.typeName, types)) {
55
+ selections.push(`${indent}${field.name}`);
56
+ }
57
+ }
58
+ if (selections.length === 0)
59
+ return "";
60
+ const outerIndent = " ".repeat(depth + 1);
61
+ return ` {\n${selections.join("\n")}\n${outerIndent}}`;
62
+ }
63
+ function isLeafType(kind, typeName, types) {
64
+ if (kind === "SCALAR" || kind === "ENUM")
65
+ return true;
66
+ const info = types.get(typeName);
67
+ return info?.kind === "SCALAR" || info?.kind === "ENUM";
68
+ }
69
+ /**
70
+ * Wraps flat params into the expected variable structure.
71
+ * Most SuperOps operations take a single `input` arg,
72
+ * so { id: "123" } becomes { input: { id: "123" } }.
73
+ */
74
+ export async function wrapVariables(operationName, params) {
75
+ const cache = await getSchemaCache();
76
+ const op = cache.operations.get(operationName);
77
+ if (!op)
78
+ throw new Error(`Unknown operation: "${operationName}"`);
79
+ if (op.args.length === 1) {
80
+ const argName = op.args[0].name;
81
+ if (!(argName in params)) {
82
+ return { [argName]: params };
83
+ }
84
+ }
85
+ return params;
86
+ }
@@ -0,0 +1,25 @@
1
+ export class ResponseFormatter {
2
+ static success(message, data) {
3
+ const structured = {
4
+ success: true,
5
+ message,
6
+ ...(data && typeof data === "object" && data !== null ? { data } : {}),
7
+ };
8
+ return {
9
+ content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
10
+ structuredContent: structured,
11
+ };
12
+ }
13
+ static error(message, details) {
14
+ const structured = {
15
+ success: false,
16
+ error: message,
17
+ ...(details && { details }),
18
+ };
19
+ return {
20
+ isError: true,
21
+ content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
22
+ structuredContent: structured,
23
+ };
24
+ }
25
+ }