@toolprint/mcp-graphql-forge
Version:
MCP server that exposes GraphQL APIs to AI tools through automatic schema introspection and tool generation
276 lines • 10.4 kB
JavaScript
import { isNonNullType, isListType, isScalarType, isEnumType, isInputObjectType, isObjectType, isInterfaceType, isUnionType, buildClientSchema } from 'graphql';
// Global cache for type field selections
const typeFieldSelectionCache = new Map();
// Cache for minimal field selections used for circular references
const minimalFieldSelectionCache = new Map();
function getGraphQLVariableType(graphqlType) {
if (isNonNullType(graphqlType)) {
return `${getGraphQLVariableType(graphqlType.ofType)}!`;
}
if (isListType(graphqlType)) {
return `[${getGraphQLVariableType(graphqlType.ofType)}]`;
}
// Return the actual GraphQL type name
return graphqlType.name;
}
// Export this function for use in CLI
export { getGraphQLVariableType };
// Helper function to get minimal field selection for circular references
function getMinimalFieldSelection(actualType) {
const typeName = actualType.name;
// Check cache first
if (minimalFieldSelectionCache.has(typeName)) {
return minimalFieldSelectionCache.get(typeName);
}
const fields = actualType.getFields();
let minimalSelection = '';
if (fields.documentId) {
minimalSelection = '{ documentId }';
}
else if (fields.id) {
minimalSelection = '{ id }';
}
else {
minimalSelection = '';
}
minimalFieldSelectionCache.set(typeName, minimalSelection);
return minimalSelection;
}
// Helper function to generate the full field selection for a type (without circular reference handling)
function generateFullFieldSelection(actualType, depth = 0) {
const typeName = actualType.name;
// Check cache first
if (typeFieldSelectionCache.has(typeName)) {
return typeFieldSelectionCache.get(typeName);
}
// Prevent infinite depth during initial generation
if (depth > 3) {
const minimal = getMinimalFieldSelection(actualType);
typeFieldSelectionCache.set(typeName, minimal);
return minimal;
}
const fields = actualType.getFields();
const fieldSelections = [];
for (const [fieldName, field] of Object.entries(fields)) {
const fieldType = field.type;
// Get the underlying type
let checkType = fieldType;
if (isNonNullType(checkType))
checkType = checkType.ofType;
if (isListType(checkType))
checkType = checkType.ofType;
if (isNonNullType(checkType))
checkType = checkType.ofType;
// For complex object/interface types
if (isObjectType(checkType) || isInterfaceType(checkType)) {
// Check if this would create a circular reference
if (checkType.name === typeName) {
// Direct self-reference - use minimal selection
const minimal = getMinimalFieldSelection(checkType);
if (minimal) {
fieldSelections.push(`${fieldName} ${minimal}`);
}
}
else {
// Generate or get cached selection for this type
const nestedSelection = generateFullFieldSelection(checkType, depth + 1);
if (nestedSelection) {
fieldSelections.push(`${fieldName} ${nestedSelection}`);
}
else {
fieldSelections.push(fieldName);
}
}
}
else if (isUnionType(checkType)) {
fieldSelections.push(`${fieldName} { __typename }`);
}
else {
// Scalar, enum, or other simple types
fieldSelections.push(fieldName);
}
}
let selection = '';
if (fieldSelections.length > 0) {
selection = `{\n${' '.repeat(depth + 1)}${fieldSelections.join(`\n${' '.repeat(depth + 1)}`)}\n${' '.repeat(depth)}}`;
}
// Cache the result
typeFieldSelectionCache.set(typeName, selection);
return selection;
}
function generateFieldSelection(type, visited = new Set(), depth = 0) {
// Handle NonNull and List wrappers to get to the actual type
let actualType = type;
if (isNonNullType(type)) {
actualType = type.ofType;
}
if (isListType(actualType)) {
actualType = actualType.ofType;
}
if (isNonNullType(actualType)) {
actualType = actualType.ofType;
}
if (isScalarType(actualType) || isEnumType(actualType)) {
return '';
}
if (isObjectType(actualType) || isInterfaceType(actualType)) {
// Check for circular references in current call stack
if (visited.has(actualType.name)) {
return getMinimalFieldSelection(actualType);
}
// Use cached full field selection if available
if (typeFieldSelectionCache.has(actualType.name)) {
return typeFieldSelectionCache.get(actualType.name);
}
// Generate full field selection and cache it
visited.add(actualType.name);
const selection = generateFullFieldSelection(actualType, depth);
visited.delete(actualType.name);
return selection;
}
if (isUnionType(actualType)) {
return '{ __typename }';
}
return '';
}
function getJSONSchemaType(graphqlType, visited = new Set()) {
if (isNonNullType(graphqlType)) {
return getJSONSchemaType(graphqlType.ofType, visited);
}
if (isListType(graphqlType)) {
return {
type: "array",
items: getJSONSchemaType(graphqlType.ofType, visited)
};
}
if (isScalarType(graphqlType)) {
switch (graphqlType.name) {
case 'String':
case 'ID':
return { type: "string" };
case 'Int':
return { type: "integer" };
case 'Float':
return { type: "number" };
case 'Boolean':
return { type: "boolean" };
default:
return { type: "string" };
}
}
if (isEnumType(graphqlType)) {
return {
type: "string",
enum: graphqlType.getValues().map(v => v.value)
};
}
if (isInputObjectType(graphqlType)) {
// Check for circular references
if (visited.has(graphqlType.name)) {
return { type: "object", description: `Circular reference to ${graphqlType.name}` };
}
visited.add(graphqlType.name);
const properties = {};
const required = [];
const fields = graphqlType.getFields();
for (const [fieldName, field] of Object.entries(fields)) {
properties[fieldName] = getJSONSchemaType(field.type, visited);
if (field.description) {
properties[fieldName].description = field.description;
}
if (isNonNullType(field.type)) {
required.push(fieldName);
}
}
visited.delete(graphqlType.name);
return {
type: "object",
properties,
...(required.length > 0 && { required })
};
}
return { type: "object" };
}
function generateToolForField(fieldName, field, operationType) {
const properties = {};
const required = [];
if (field.args && field.args.length > 0) {
for (const arg of field.args) {
properties[arg.name] = getJSONSchemaType(arg.type);
if (arg.description) {
properties[arg.name].description = arg.description;
}
// Only mark as required if it's NonNull AND doesn't have a default value
if (isNonNullType(arg.type) && arg.defaultValue === undefined) {
required.push(arg.name);
}
}
}
// Generate field selection for the return type
const fieldSelection = generateFieldSelection(field.type);
return {
name: `${operationType}_${fieldName}`,
description: field.description || `Execute GraphQL ${operationType}: ${fieldName}`,
inputSchema: {
type: "object",
properties,
...(required.length > 0 && { required })
},
_graphql: {
fieldName,
operationType,
args: field.args || [],
returnType: field.type,
fieldSelection
}
};
}
// Function to clear caches when processing a new schema
function clearFieldSelectionCaches() {
typeFieldSelectionCache.clear();
minimalFieldSelectionCache.clear();
}
// Function to export cache for debugging
export function getFieldSelectionCache() {
return {
full: Object.fromEntries(typeFieldSelectionCache),
minimal: Object.fromEntries(minimalFieldSelectionCache)
};
}
export function generateMCPToolsFromSchema(introspectionResult) {
// Clear caches for fresh schema processing
clearFieldSelectionCaches();
const schema = buildClientSchema(introspectionResult);
const tools = [];
console.log('🗂️ Building field selection cache for all types...');
// Pre-generate field selections for all object types to populate cache
const typeMap = schema.getTypeMap();
let cacheHits = 0;
let cacheGenerations = 0;
for (const [typeName, type] of Object.entries(typeMap)) {
if (isObjectType(type) || isInterfaceType(type)) {
if (!typeName.startsWith('__')) { // Skip introspection types
generateFullFieldSelection(type);
cacheGenerations++;
}
}
}
console.log(`📊 Generated field selections for ${cacheGenerations} types`);
const queryType = schema.getQueryType();
if (queryType) {
const fields = queryType.getFields();
for (const [fieldName, field] of Object.entries(fields)) {
tools.push(generateToolForField(fieldName, field, 'query'));
}
}
const mutationType = schema.getMutationType();
if (mutationType) {
const fields = mutationType.getFields();
for (const [fieldName, field] of Object.entries(fields)) {
tools.push(generateToolForField(fieldName, field, 'mutation'));
}
}
console.log(`💾 Field selection cache contains ${typeFieldSelectionCache.size} full selections and ${minimalFieldSelectionCache.size} minimal selections`);
return tools;
}
//# sourceMappingURL=tool-generator.js.map