drupal-mcp-connector 0.6.1
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/CHANGELOG.md +92 -0
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/config/config.example.json +122 -0
- package/package.json +70 -0
- package/src/index.js +499 -0
- package/src/lib/backends/backend-interface.js +164 -0
- package/src/lib/backends/errors.js +31 -0
- package/src/lib/backends/graphql-filter.js +99 -0
- package/src/lib/backends/graphql-names.js +63 -0
- package/src/lib/backends/graphql-normalize.js +73 -0
- package/src/lib/backends/graphql-query.js +129 -0
- package/src/lib/backends/graphql-schema.js +226 -0
- package/src/lib/backends/graphql.js +391 -0
- package/src/lib/backends/index.js +128 -0
- package/src/lib/backends/jsonapi.js +403 -0
- package/src/lib/canonical.js +68 -0
- package/src/lib/config.js +257 -0
- package/src/lib/drupal-fetch.js +144 -0
- package/src/lib/errors.js +38 -0
- package/src/lib/http-auth.js +27 -0
- package/src/lib/oauth.js +177 -0
- package/src/lib/reports-support.js +75 -0
- package/src/lib/security.js +475 -0
- package/src/lib/validate.js +225 -0
- package/src/tools/drush.js +463 -0
- package/src/tools/entities.js +262 -0
- package/src/tools/graphql.js +175 -0
- package/src/tools/media.js +297 -0
- package/src/tools/nodes.js +247 -0
- package/src/tools/reports.js +609 -0
- package/src/tools/site.js +87 -0
- package/src/tools/taxonomy.js +202 -0
- package/src/tools/users.js +250 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool group: Generic entities.
|
|
3
|
+
*
|
|
4
|
+
* Type-agnostic CRUD plus schema/type discovery and a security summary. These
|
|
5
|
+
* tools work with ANY Drupal entity type + bundle, so each handler asserts the
|
|
6
|
+
* appropriate read/write/delete permission in-handler (the name-prefix gating
|
|
7
|
+
* in index.js cannot know the entity type from the generic tool names). Reads
|
|
8
|
+
* are redacted per the site policy.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getSiteConfig } from "../lib/config.js";
|
|
12
|
+
import { resolveBackend } from "../lib/backends/index.js";
|
|
13
|
+
import {
|
|
14
|
+
resolveSecurityConfig, assertReadAllowed, assertWriteAllowed, assertDeleteAllowed,
|
|
15
|
+
redactCanonicalEntity, getSecuritySummary,
|
|
16
|
+
} from "../lib/security.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* List entities of any type/bundle with filters, sort, includes and paging.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} args - { site?, entityType, bundle, filters?, sort?, limit?, offset?, include? }.
|
|
22
|
+
* @returns {Promise<{total: number, approximate: boolean, offset: number,
|
|
23
|
+
* nextOffset: number, entities: object[]}>} Paged, redacted entity list.
|
|
24
|
+
* @throws {SecurityError} If reading the type/bundle is not permitted.
|
|
25
|
+
*/
|
|
26
|
+
async function listEntities({ site: siteName, entityType, bundle, filters = [], sort = [], limit = 20, offset = 0, include = [] }) {
|
|
27
|
+
const site = getSiteConfig(siteName);
|
|
28
|
+
const sec = resolveSecurityConfig(site);
|
|
29
|
+
assertReadAllowed(sec, entityType, bundle);
|
|
30
|
+
const backend = await resolveBackend(site);
|
|
31
|
+
const res = await backend.listEntities({ entityType, bundle, filters, sort, include, page: { limit, offset } });
|
|
32
|
+
const entities = res.entities.map((e) => redactCanonicalEntity(e, sec, entityType));
|
|
33
|
+
return { total: res.page?.total ?? entities.length, approximate: res.approximate ?? false, offset, nextOffset: offset + entities.length, entities };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fetch a single entity of any type by UUID, redacted per policy.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} args - { site?, entityType, bundle, id, include? }.
|
|
40
|
+
* @returns {Promise<object|null>} The redacted entity, or null if not found.
|
|
41
|
+
* @throws {SecurityError} If reading the type/bundle is not permitted.
|
|
42
|
+
*/
|
|
43
|
+
async function getEntity({ site: siteName, entityType, bundle, id, include = [] }) {
|
|
44
|
+
const site = getSiteConfig(siteName);
|
|
45
|
+
const sec = resolveSecurityConfig(site);
|
|
46
|
+
assertReadAllowed(sec, entityType, bundle);
|
|
47
|
+
const backend = await resolveBackend(site);
|
|
48
|
+
const entity = await backend.getEntity({ entityType, bundle, id, include });
|
|
49
|
+
return entity ? redactCanonicalEntity(entity, sec, entityType) : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create an entity of any type/bundle.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} args - { site?, entityType, bundle, attributes?, relationships? }.
|
|
56
|
+
* @returns {Promise<object>} The created entity descriptor.
|
|
57
|
+
* @throws {SecurityError} If creating the type/bundle is not permitted.
|
|
58
|
+
*/
|
|
59
|
+
async function createEntity({ site: siteName, entityType, bundle, attributes = {}, relationships = {} }) {
|
|
60
|
+
const site = getSiteConfig(siteName);
|
|
61
|
+
const sec = resolveSecurityConfig(site);
|
|
62
|
+
assertWriteAllowed(sec, "create", entityType, bundle);
|
|
63
|
+
const backend = await resolveBackend(site);
|
|
64
|
+
return backend.createEntity({ entityType, bundle, attributes, relationships });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Update an entity of any type/bundle (partial — only supplied fields are sent).
|
|
69
|
+
*
|
|
70
|
+
* @param {object} args - { site?, entityType, bundle, id, attributes?, relationships? }.
|
|
71
|
+
* @returns {Promise<object>} The updated entity descriptor.
|
|
72
|
+
* @throws {SecurityError} If updating the type/bundle is not permitted.
|
|
73
|
+
*/
|
|
74
|
+
async function updateEntity({ site: siteName, entityType, bundle, id, attributes = {}, relationships = {} }) {
|
|
75
|
+
const site = getSiteConfig(siteName);
|
|
76
|
+
const sec = resolveSecurityConfig(site);
|
|
77
|
+
assertWriteAllowed(sec, "update", entityType, bundle);
|
|
78
|
+
const backend = await resolveBackend(site);
|
|
79
|
+
return backend.updateEntity({ entityType, bundle, id, attributes, relationships });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Delete an entity of any type/bundle. Requires allowDestructive in policy.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} args - { site?, entityType, bundle, id }.
|
|
86
|
+
* @returns {Promise<{success: boolean, deletedId: string, entityType: string, bundle: string}>}
|
|
87
|
+
* @throws {SecurityError} If deleting the type/bundle is not permitted.
|
|
88
|
+
*/
|
|
89
|
+
async function deleteEntity({ site: siteName, entityType, bundle, id }) {
|
|
90
|
+
const site = getSiteConfig(siteName);
|
|
91
|
+
const sec = resolveSecurityConfig(site);
|
|
92
|
+
assertDeleteAllowed(sec, entityType, bundle, id);
|
|
93
|
+
const backend = await resolveBackend(site);
|
|
94
|
+
await backend.deleteEntity({ entityType, bundle, id });
|
|
95
|
+
return { success: true, deletedId: id, entityType, bundle };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Discover all resource types the backend exposes, filtered to those the policy
|
|
100
|
+
* permits reading. Accessibility is probed per type by catching the assertion
|
|
101
|
+
* (rather than indexing a policy table), which keeps the lookup injection-safe.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} args - { site? }.
|
|
104
|
+
* @returns {Promise<{total: number, accessible: number, blocked: number,
|
|
105
|
+
* resourceTypes: object[]}>} Counts plus the list of readable types.
|
|
106
|
+
*/
|
|
107
|
+
async function listEntityTypes({ site: siteName }) {
|
|
108
|
+
const site = getSiteConfig(siteName);
|
|
109
|
+
const sec = resolveSecurityConfig(site);
|
|
110
|
+
const backend = await resolveBackend(site);
|
|
111
|
+
const all = await backend.listResourceTypes();
|
|
112
|
+
const accessible = all.filter(({ entityType, bundle }) => {
|
|
113
|
+
try { assertReadAllowed(sec, entityType, bundle); return true; } catch { return false; }
|
|
114
|
+
});
|
|
115
|
+
return { total: all.length, accessible: accessible.length, blocked: all.length - accessible.length, resourceTypes: accessible };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Return the field/relationship schema for a type + bundle.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} args - { site?, entityType, bundle }.
|
|
122
|
+
* @returns {Promise<object>} The backend's schema descriptor.
|
|
123
|
+
* @throws {SecurityError} If reading the type/bundle is not permitted.
|
|
124
|
+
*/
|
|
125
|
+
async function getEntitySchema({ site: siteName, entityType, bundle }) {
|
|
126
|
+
const site = getSiteConfig(siteName);
|
|
127
|
+
const sec = resolveSecurityConfig(site);
|
|
128
|
+
assertReadAllowed(sec, entityType, bundle);
|
|
129
|
+
const backend = await resolveBackend(site);
|
|
130
|
+
return backend.getEntitySchema(entityType, bundle);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Summarize the active security policy for a site (allowed/blocked/redacted).
|
|
135
|
+
* No backend call — reads policy only.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} args - { site? }.
|
|
138
|
+
* @returns {Promise<object>} The security summary.
|
|
139
|
+
*/
|
|
140
|
+
async function securityInfo({ site: siteName }) {
|
|
141
|
+
const site = getSiteConfig(siteName);
|
|
142
|
+
return getSecuritySummary(site);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Definitions
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
export const definitions = [
|
|
150
|
+
{
|
|
151
|
+
name: "drupal_list_entity_types",
|
|
152
|
+
description: "Discover all JSON:API resource types (entity types + bundles) exposed by this Drupal site, filtered to only those your security config allows. Run this before working with an unfamiliar entity type.",
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: "object",
|
|
155
|
+
properties: { site: { type: "string" } },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "drupal_get_entity_schema",
|
|
160
|
+
description: "Inspect the fields and relationships available on any Drupal entity type + bundle. Run this before creating or updating entities to know what fields are available.",
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: "object", required: ["entityType", "bundle"],
|
|
163
|
+
properties: {
|
|
164
|
+
site: { type: "string" },
|
|
165
|
+
entityType: { type: "string", description: "e.g. 'node', 'paragraph', 'commerce_product', 'block_content'" },
|
|
166
|
+
bundle: { type: "string", description: "e.g. 'article', 'text', 'default'" },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "drupal_entity_list",
|
|
172
|
+
description: "List entities of any Drupal entity type and bundle. Supports structured filters, sorting, pagination, and relationship includes. Use drupal_list_entity_types first to discover available types.",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: "object", required: ["entityType", "bundle"],
|
|
175
|
+
properties: {
|
|
176
|
+
site: { type: "string" },
|
|
177
|
+
entityType: { type: "string", description: "Entity type machine name, e.g. 'paragraph', 'block_content', 'commerce_product'" },
|
|
178
|
+
bundle: { type: "string", description: "Bundle machine name" },
|
|
179
|
+
filters: { type: "array", description: "Structured filters: [{ field, op, value }]", items: { type: "object" } },
|
|
180
|
+
sort: { type: "array", description: "Sort specs: [{ field, dir }]", items: { type: "object" } },
|
|
181
|
+
include: { type: "array", description: "Relationship field names to sideload", items: { type: "string" } },
|
|
182
|
+
limit: { type: "number", default: 20 },
|
|
183
|
+
offset: { type: "number", default: 0 },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "drupal_entity_get",
|
|
189
|
+
description: "Fetch a single entity of any Drupal entity type by UUID.",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: "object", required: ["entityType", "bundle", "id"],
|
|
192
|
+
properties: {
|
|
193
|
+
site: { type: "string" },
|
|
194
|
+
entityType: { type: "string" },
|
|
195
|
+
bundle: { type: "string" },
|
|
196
|
+
id: { type: "string", description: "Entity UUID" },
|
|
197
|
+
include: { type: "array", description: "Relationship field names to sideload", items: { type: "string" } },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: "drupal_entity_create",
|
|
203
|
+
description: "Create an entity of any Drupal entity type and bundle. Use drupal_get_entity_schema first to know what fields are available. All operations checked against security config.",
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: "object", required: ["entityType", "bundle"],
|
|
206
|
+
properties: {
|
|
207
|
+
site: { type: "string" },
|
|
208
|
+
entityType: { type: "string" },
|
|
209
|
+
bundle: { type: "string" },
|
|
210
|
+
attributes: { type: "object", description: "Field values keyed by Drupal machine name" },
|
|
211
|
+
relationships: { type: "object", description: "Relationship data keyed by field name" },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "drupal_entity_update",
|
|
217
|
+
description: "Update an existing entity of any Drupal entity type. Only include attributes/relationships you want to change.",
|
|
218
|
+
inputSchema: {
|
|
219
|
+
type: "object", required: ["entityType", "bundle", "id"],
|
|
220
|
+
properties: {
|
|
221
|
+
site: { type: "string" },
|
|
222
|
+
entityType: { type: "string" },
|
|
223
|
+
bundle: { type: "string" },
|
|
224
|
+
id: { type: "string" },
|
|
225
|
+
attributes: { type: "object" },
|
|
226
|
+
relationships: { type: "object" },
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "drupal_entity_delete",
|
|
232
|
+
description: "Delete an entity of any Drupal entity type. Requires allowDestructive = true in security config. Confirm with the user before calling.",
|
|
233
|
+
inputSchema: {
|
|
234
|
+
type: "object", required: ["entityType", "bundle", "id"],
|
|
235
|
+
properties: {
|
|
236
|
+
site: { type: "string" },
|
|
237
|
+
entityType: { type: "string" },
|
|
238
|
+
bundle: { type: "string" },
|
|
239
|
+
id: { type: "string" },
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: "drupal_security_info",
|
|
245
|
+
description: "Show the active security configuration for a site — what's allowed, what's blocked, what fields are redacted. Run this to understand the current access policy.",
|
|
246
|
+
inputSchema: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: { site: { type: "string" } },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
export const handlers = {
|
|
254
|
+
drupal_list_entity_types: listEntityTypes,
|
|
255
|
+
drupal_get_entity_schema: getEntitySchema,
|
|
256
|
+
drupal_entity_list: listEntities,
|
|
257
|
+
drupal_entity_get: getEntity,
|
|
258
|
+
drupal_entity_create: createEntity,
|
|
259
|
+
drupal_entity_update: updateEntity,
|
|
260
|
+
drupal_entity_delete: deleteEntity,
|
|
261
|
+
drupal_security_info: securityInfo,
|
|
262
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool group: GraphQL.
|
|
3
|
+
*
|
|
4
|
+
* Raw GraphQL execution and schema introspection against a Drupal site. Require
|
|
5
|
+
* the GraphQL Compose module (drupal.org/project/graphql_compose), which
|
|
6
|
+
* exposes a read-only schema (no mutations); any mutation in a query is
|
|
7
|
+
* additionally gated by the per-site "allowGraphqlMutations" security flag,
|
|
8
|
+
* enforced by the middleware in index.js before the handler runs.
|
|
9
|
+
*
|
|
10
|
+
* Per-site config: set "graphqlEndpoint" to override "/graphql".
|
|
11
|
+
* Auth reuses the same credentials as the JSON:API tools.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getSiteConfig } from "../lib/config.js";
|
|
15
|
+
import { drupalGraphqlFetch } from "../lib/drupal-fetch.js";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Implementations
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Execute a GraphQL query or mutation and normalize the response envelope.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} args - { site?, query, variables?, operationName? }.
|
|
25
|
+
* @returns {Promise<{data: object, warnings?: string[]}>} On a clean response,
|
|
26
|
+
* just `data`. When the server returns errors alongside partial `data`, the
|
|
27
|
+
* error messages are surfaced as `warnings` rather than thrown.
|
|
28
|
+
* @throws {Error} If the response carries errors and no data at all.
|
|
29
|
+
*/
|
|
30
|
+
async function runGraphql({ site: siteName, query, variables = {}, operationName }) {
|
|
31
|
+
const site = getSiteConfig(siteName);
|
|
32
|
+
const json = await drupalGraphqlFetch(site, { query, variables, operationName });
|
|
33
|
+
|
|
34
|
+
if (json.errors?.length) {
|
|
35
|
+
const messages = json.errors.map((e) => e.message).join("; ");
|
|
36
|
+
if (!json.data) throw new Error(`GraphQL errors: ${messages}`);
|
|
37
|
+
// Partial result — return data AND surface errors as a warning
|
|
38
|
+
return { data: json.data, warnings: json.errors.map((e) => e.message) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { data: json.data };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Introspect the GraphQL schema, either in detail for one type or as an
|
|
46
|
+
* overview. When `typeName` is given, returns that type's fields, args, and
|
|
47
|
+
* input fields. Otherwise returns the root operation types plus all non
|
|
48
|
+
* built-in, non-scalar types for readability.
|
|
49
|
+
*
|
|
50
|
+
* @param {object} args - { site?, typeName? }.
|
|
51
|
+
* @returns {Promise<object>} A single __type descriptor, or a schema overview.
|
|
52
|
+
* @throws {Error} If a requested typeName is not present in the schema, or the
|
|
53
|
+
* overview query returns errors.
|
|
54
|
+
*/
|
|
55
|
+
async function introspectGraphql({ site: siteName, typeName }) {
|
|
56
|
+
const site = getSiteConfig(siteName);
|
|
57
|
+
|
|
58
|
+
// If a specific type is requested, get detailed field info for it.
|
|
59
|
+
if (typeName) {
|
|
60
|
+
const query = `
|
|
61
|
+
query IntrospectType($name: String!) {
|
|
62
|
+
__type(name: $name) {
|
|
63
|
+
name
|
|
64
|
+
kind
|
|
65
|
+
description
|
|
66
|
+
fields(includeDeprecated: true) {
|
|
67
|
+
name
|
|
68
|
+
description
|
|
69
|
+
isDeprecated
|
|
70
|
+
deprecationReason
|
|
71
|
+
type { name kind ofType { name kind ofType { name kind } } }
|
|
72
|
+
args { name description type { name kind ofType { name kind } } }
|
|
73
|
+
}
|
|
74
|
+
inputFields {
|
|
75
|
+
name
|
|
76
|
+
description
|
|
77
|
+
type { name kind ofType { name kind } }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
const json = await drupalGraphqlFetch(site, { query, variables: { name: typeName } });
|
|
83
|
+
if (!json.data?.__type) throw new Error(`Type '${typeName}' not found in schema.`);
|
|
84
|
+
return json.data.__type;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Otherwise return a high-level schema overview.
|
|
88
|
+
const query = `
|
|
89
|
+
{
|
|
90
|
+
__schema {
|
|
91
|
+
queryType { name }
|
|
92
|
+
mutationType { name }
|
|
93
|
+
subscriptionType { name }
|
|
94
|
+
types {
|
|
95
|
+
name
|
|
96
|
+
kind
|
|
97
|
+
description
|
|
98
|
+
fields { name type { name kind ofType { name kind } } }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
const json = await drupalGraphqlFetch(site, { query });
|
|
104
|
+
if (json.errors?.length) {
|
|
105
|
+
throw new Error(json.errors.map((e) => e.message).join("; "));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const schema = json.data.__schema;
|
|
109
|
+
// Filter out built-in introspection types (__*) and scalars for readability
|
|
110
|
+
const types = schema.types.filter(
|
|
111
|
+
(t) => !t.name.startsWith("__") && t.kind !== "SCALAR"
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
queryType: schema.queryType?.name ?? null,
|
|
116
|
+
mutationType: schema.mutationType?.name ?? null,
|
|
117
|
+
subscriptionType: schema.subscriptionType?.name ?? null,
|
|
118
|
+
types,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Definitions
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
export const definitions = [
|
|
127
|
+
{
|
|
128
|
+
name: "drupal_graphql",
|
|
129
|
+
description: `Execute a GraphQL query against a Drupal site.
|
|
130
|
+
Requires the GraphQL Compose module (drupal.org/project/graphql_compose), which
|
|
131
|
+
exposes a read-only schema; mutations are gated by "allowGraphqlMutations".
|
|
132
|
+
Use drupal_graphql_introspect first to discover available types and fields.
|
|
133
|
+
|
|
134
|
+
Example query:
|
|
135
|
+
query GetArticle($id: String!) {
|
|
136
|
+
nodeById(id: $id) {
|
|
137
|
+
title
|
|
138
|
+
... on NodeArticle { body { value } }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Example mutation (only if your GraphQL Compose schema enables mutations):
|
|
143
|
+
mutation CreateArticle($title: String!, $body: String!) {
|
|
144
|
+
createNodeArticle(data: { title: $title, body: { value: $body, format: "full_html" } }) {
|
|
145
|
+
entity { title uuid }
|
|
146
|
+
errors { message }
|
|
147
|
+
}
|
|
148
|
+
}`,
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: "object", required: ["query"],
|
|
151
|
+
properties: {
|
|
152
|
+
site: { type: "string", description: "Named site (omit for default)" },
|
|
153
|
+
query: { type: "string", description: "GraphQL query or mutation string" },
|
|
154
|
+
variables: { type: "object", description: "Variables to pass with the query" },
|
|
155
|
+
operationName: { type: "string", description: "Operation name (for multi-operation documents)" },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "drupal_graphql_introspect",
|
|
161
|
+
description: "Introspect the Drupal GraphQL schema. Omit typeName for a full schema overview; provide typeName to get detailed fields and args for a specific type.",
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
site: { type: "string" },
|
|
166
|
+
typeName: { type: "string", description: "Name of a specific GraphQL type to inspect in detail (optional)" },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
export const handlers = {
|
|
173
|
+
drupal_graphql: runGraphql,
|
|
174
|
+
drupal_graphql_introspect: introspectGraphql,
|
|
175
|
+
};
|