@convisoappsec/mcp 0.3.0 → 0.5.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,117 @@
1
+ /**
2
+ * Generic, catalog-driven mutation engine.
3
+ *
4
+ * Loads the build-time catalog (mutations_catalog.json, produced from sdl.gql by
5
+ * scripts/generate_mutation_catalog.mjs) and exposes pure helpers — no network — that
6
+ * let the MCP discover, describe, and build a GraphQL string for ANY of the platform's
7
+ * mutations. Execution itself goes through GraphQLClient.execute().
8
+ *
9
+ * Every Conviso mutation is Relay-style: a single `input: <Name>Input!` argument
10
+ * returning `<Name>Payload`. buildMutationQuery stays generic over `args` anyway, so it
11
+ * keeps working if the schema ever adds a multi-argument mutation.
12
+ */
13
+ import catalog from './mutations_catalog.json' with { type: 'json' };
14
+
15
+ const byName = new Map(catalog.mutations.map((m) => [m.name, m]));
16
+
17
+ export function getCatalog() {
18
+ return catalog;
19
+ }
20
+
21
+ export function isKnownMutation(name) {
22
+ return byName.has(name);
23
+ }
24
+
25
+ /** Strip list/non-null decoration to get the base type name (e.g. "[Foo!]!" -> "Foo"). */
26
+ function namedOf(typeStr) {
27
+ return String(typeStr).replace(/[[\]!]/g, '').trim();
28
+ }
29
+
30
+ /**
31
+ * Discover mutations. Filters by case-insensitive substring on name/description and by
32
+ * coarse category. Returns lightweight rows so the model can pick one, then call
33
+ * describe_mutation for its full input schema.
34
+ */
35
+ export function listMutations({ search = '', category = null, limit = 50 } = {}) {
36
+ const q = (search || '').toLowerCase();
37
+ let out = catalog.mutations;
38
+ if (category) out = out.filter((m) => m.category === category);
39
+ if (q) {
40
+ out = out.filter(
41
+ (m) => m.name.toLowerCase().includes(q) || (m.description || '').toLowerCase().includes(q)
42
+ );
43
+ }
44
+ const total = out.length;
45
+ const mutations = out.slice(0, Math.max(0, limit)).map((m) => ({
46
+ name: m.name,
47
+ description: m.description,
48
+ category: m.category,
49
+ destructive: m.destructive,
50
+ }));
51
+ return { total, count: mutations.length, mutations };
52
+ }
53
+
54
+ /** Recursively expand an input object type into a field tree (nested inputs to `maxDepth`). */
55
+ function expandInput(typeName, depth, seen, maxDepth = 2) {
56
+ const fields = catalog.inputs[typeName];
57
+ if (!fields) return null;
58
+ if (seen.has(typeName)) return `<recursive ${typeName}>`;
59
+ const nextSeen = new Set(seen);
60
+ nextSeen.add(typeName);
61
+ return fields.map((f) => {
62
+ const base = namedOf(f.type);
63
+ const d = { name: f.name, type: f.type, required: !!f.required };
64
+ if (f.description) d.description = f.description;
65
+ if (catalog.enums[base]) {
66
+ d.enumValues = catalog.enums[base];
67
+ } else if (catalog.inputs[base] && depth < maxDepth) {
68
+ d.fields = expandInput(base, depth + 1, nextSeen, maxDepth);
69
+ }
70
+ return d;
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Full schema for one mutation: argument list, expanded input fields (with enum values
76
+ * and nested input objects inlined), and the default payload selection used when the
77
+ * caller does not pass return_fields.
78
+ */
79
+ export function describeMutation(name) {
80
+ const m = byName.get(name);
81
+ if (!m) {
82
+ return {
83
+ error: `Unknown mutation '${name}'. Call list_mutations to discover valid names.`,
84
+ };
85
+ }
86
+ return {
87
+ name: m.name,
88
+ description: m.description,
89
+ category: m.category,
90
+ destructive: m.destructive,
91
+ args: m.args,
92
+ inputType: m.inputType,
93
+ input: m.inputType ? expandInput(m.inputType, 0, new Set()) : null,
94
+ payloadType: m.payloadType,
95
+ defaultReturnFields: m.payloadSelection,
96
+ usage: `execute_mutation({ name: "${m.name}", variables: { input: { ... } } })`,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Build the GraphQL document + variables for a catalog mutation. Pure and side-effect
102
+ * free so it can be unit-tested without a network. Throws on unknown names — only
103
+ * catalogued mutations are runnable (whitelist; no raw-string passthrough).
104
+ */
105
+ export function buildMutationQuery(name, variables = {}, returnFields = null) {
106
+ const m = byName.get(name);
107
+ if (!m) {
108
+ throw new Error(`Unknown mutation '${name}'. Call list_mutations to discover valid names.`);
109
+ }
110
+ const opName = name.charAt(0).toUpperCase() + name.slice(1);
111
+ const decls = m.args.map((a) => `$${a.name}: ${a.type}`).join(', ');
112
+ const pass = m.args.map((a) => `${a.name}: $${a.name}`).join(', ');
113
+ const sel = (returnFields && String(returnFields).trim()) || m.payloadSelection || 'clientMutationId';
114
+ const selBlock = sel ? ` {\n ${sel}\n }` : '';
115
+ const query = `mutation ${opName}(${decls}) {\n ${name}(${pass})${selBlock}\n}`;
116
+ return { query, variables };
117
+ }