import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import axios from "axios";
import {
buildClientSchema,
getIntrospectionQuery,
GraphQLObjectType,
} from "graphql";
import { z } from "zod";
interface MCPTypeInfo {
name: string;
fields: string[];
description?: string;
}
interface MCPSchemaInfo {
types: MCPTypeInfo[];
queries: MCPTypeInfo[];
mutations: MCPTypeInfo[];
inputs: MCPTypeInfo[];
}
interface SearchResult {
category: string;
matches: {
item: any;
relevance: number;
matchedOn: string[];
context?: string;
}[];
}
function calculateLevenshteinDistance(a: string, b: string): number {
const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));
for (let i = 0; i <= a.length; i++) matrix[0][i] = i;
for (let j = 0; j <= b.length; j++) matrix[j][0] = j;
for (let j = 1; j <= b.length; j++) {
for (let i = 1; i <= a.length; i++) {
const substitutionCost = a[i - 1] === b[j - 1] ? 0 : 1;
matrix[j][i] = Math.min(
matrix[j][i - 1] + 1,
matrix[j - 1][i] + 1,
matrix[j - 1][i - 1] + substitutionCost
);
}
}
return matrix[b.length][a.length];
}
function calculateSimilarity(str1: string, str2: string): number {
const s1 = str1.toLowerCase();
const s2 = str2.toLowerCase();
if (s1 === s2) return 1;
if (s1.includes(s2) || s2.includes(s1)) return 0.9;
const maxLength = Math.max(s1.length, s2.length);
const distance = calculateLevenshteinDistance(s1, s2);
return 1 - distance / maxLength;
}
function getRelevantContext(item: any, matchedFields: string[]): string {
const context: string[] = [];
if (item.description) {
context.push(`Description: ${item.description}`);
}
if (item.fields?.length) {
const relevantFields = item.fields
.filter((f: string) => matchedFields.includes(f) || matchedFields.some((m: string) => f.includes(m)))
.slice(0, 3);
if (relevantFields.length) {
context.push(`Related fields: ${relevantFields.join(', ')}`);
}
}
return context.join(' | ');
}
async function introspectSchema(endpoint: string): Promise<MCPSchemaInfo> {
const response = await axios.post(endpoint, {
query: getIntrospectionQuery(),
});
const { data } = response.data;
const schema = buildClientSchema(data);
const types: MCPTypeInfo[] = [];
const queries: MCPTypeInfo[] = [];
const mutations: MCPTypeInfo[] = [];
const inputs: MCPTypeInfo[] = [];
Object.values(schema.getTypeMap()).forEach((type) => {
if (!type.name.startsWith("__")) {
const typeInfo: MCPTypeInfo = {
name: type.name,
fields: [],
description: type.description || undefined,
};
if (type instanceof GraphQLObjectType) {
typeInfo.fields = Object.keys(type.getFields());
}
if (type.name.endsWith("Input")) {
inputs.push(typeInfo);
} else if (!type.name.startsWith("_")) {
types.push(typeInfo);
}
}
});
const queryType = schema.getQueryType();
const mutationType = schema.getMutationType();
if (queryType) {
Object.entries(queryType.getFields()).forEach(([name, field]) => {
queries.push({
name,
fields: field.args.map((arg) => arg.name),
description: field.description || undefined,
});
});
}
if (mutationType) {
Object.entries(mutationType.getFields()).forEach(([name, field]) => {
mutations.push({
name,
fields: field.args.map((arg) => arg.name),
description: field.description || undefined,
});
});
}
return { types, queries, mutations, inputs };
}
async function enhancedSearch(schema: MCPSchemaInfo, searchTerm: string, options: { threshold?: number } = {}): Promise<SearchResult[]> {
const threshold = options.threshold || 0.3;
const results: SearchResult[] = [];
const terms = searchTerm.toLowerCase().split(/\s+/);
function matchItem(item: any, searchIn: string[]): { matched: boolean; relevance: number; matchedOn: string[] } {
let maxRelevance = 0;
const matchedOn: string[] = [];
for (const field of searchIn) {
const value = item[field];
if (!value) continue;
const stringValue = typeof value === 'string' ? value :
Array.isArray(value) ? value.join(' ') :
String(value);
for (const term of terms) {
const similarity = calculateSimilarity(stringValue.toLowerCase(), term);
if (similarity > threshold) {
maxRelevance = Math.max(maxRelevance, similarity);
if (!matchedOn.includes(field)) matchedOn.push(field);
}
}
}
return {
matched: maxRelevance > threshold,
relevance: maxRelevance,
matchedOn
};
}
// Search in types
const typeMatches = schema.types
.map(type => {
const match = matchItem(type, ['name', 'description', 'fields']);
if (!match.matched) return null;
return {
item: type,
relevance: match.relevance,
matchedOn: match.matchedOn,
context: getRelevantContext(type, terms)
};
})
.filter(Boolean)
.sort((a, b) => b!.relevance - a!.relevance);
if (typeMatches.length) {
results.push({ category: "Types", matches: typeMatches as any[] });
}
// Search in queries
const queryMatches = schema.queries
.map(query => {
const match = matchItem(query, ['name', 'description', 'fields']);
if (!match.matched) return null;
return {
item: query,
relevance: match.relevance,
matchedOn: match.matchedOn,
context: getRelevantContext(query, terms)
};
})
.filter(Boolean)
.sort((a, b) => b!.relevance - a!.relevance);
if (queryMatches.length) {
results.push({ category: "Queries", matches: queryMatches as any[] });
}
// Search in mutations
const mutationMatches = schema.mutations
.map(mutation => {
const match = matchItem(mutation, ['name', 'description', 'fields']);
if (!match.matched) return null;
return {
item: mutation,
relevance: match.relevance,
matchedOn: match.matchedOn,
context: getRelevantContext(mutation, terms)
};
})
.filter(Boolean)
.sort((a, b) => b!.relevance - a!.relevance);
if (mutationMatches.length) {
results.push({ category: "Mutations", matches: mutationMatches as any[] });
}
// Search in inputs
const inputMatches = schema.inputs
.map(input => {
const match = matchItem(input, ['name', 'description', 'fields']);
if (!match.matched) return null;
return {
item: input,
relevance: match.relevance,
matchedOn: match.matchedOn,
context: getRelevantContext(input, terms)
};
})
.filter(Boolean)
.sort((a, b) => b!.relevance - a!.relevance);
if (inputMatches.length) {
results.push({ category: "Inputs", matches: inputMatches as any[] });
}
return results;
}
async function main() {
const GRAPHQL_ENDPOINT = "http://localhost:3000/graphql";
const server = new McpServer({
name: "cw-core",
version: "1.0.0",
});
server.registerTool(
"schema",
{
title: "GraphQL Schema",
description: "Full introspection of GraphQL schema"
},
async () => {
const schema = await introspectSchema(GRAPHQL_ENDPOINT);
return {
content: [{ type: "text", text: JSON.stringify(schema, null, 2) }],
};
}
);
server.registerTool(
"queries",
{
title: "GraphQL Queries",
description: "All available GraphQL queries"
},
async () => {
const schema = await introspectSchema(GRAPHQL_ENDPOINT);
return {
content: [{ type: "text", text: JSON.stringify(schema.queries, null, 2) }],
};
}
);
server.registerTool(
"mutations",
{
title: "GraphQL Mutations",
description: "All available GraphQL mutations"
},
async () => {
const schema = await introspectSchema(GRAPHQL_ENDPOINT);
return {
content: [{ type: "text", text: JSON.stringify(schema.mutations, null, 2) }],
};
}
);
server.registerTool(
"types",
{
title: "GraphQL Types",
description: "Get fields from a specific GraphQL type",
inputSchema: {
typeName: z.string().describe("Name of the GraphQL type to inspect")
}
},
async (input) => {
const { typeName } = input;
if (!typeName) throw new Error("Missing 'typeName' argument");
const schema = await introspectSchema(GRAPHQL_ENDPOINT);
const type =
schema.types.find((t) => t.name === typeName) ||
schema.inputs.find((t) => t.name === typeName);
if (!type) {
throw new Error(`Type '${typeName}' not found`);
}
return {
content: [{ type: "text", text: JSON.stringify(type, null, 2) }],
};
}
);
server.registerTool(
"search",
{
title: "Search Schema",
description: "Advanced search across all GraphQL schema elements",
inputSchema: {
searchTerm: z.string().describe("Search term - supports multiple words and partial matches"),
threshold: z.number().optional().describe("Similarity threshold (0-1, default: 0.3)")
}
},
async (input) => {
const { searchTerm, threshold } = input;
if (!searchTerm) throw new Error("Missing 'searchTerm' argument");
const schema = await introspectSchema(GRAPHQL_ENDPOINT);
const results = await enhancedSearch(schema, searchTerm, { threshold });
// Format results for better readability
const formattedResults = results.map(category => ({
category: category.category,
matches: category.matches.map(match => ({
name: match.item.name,
relevance: Math.round(match.relevance * 100) + "%",
matchedIn: match.matchedOn.join(", "),
context: match.context,
details: match.item
}))
}));
return {
content: [{
type: "text",
text: JSON.stringify(formattedResults, null, 2)
}],
};
}
);
server.registerTool(
"field",
{
title: "Field Details",
description: "Get detailed information about a specific field in a type",
inputSchema: {
typeName: z.string().describe("Name of the GraphQL type containing the field"),
fieldName: z.string().describe("Name of the field to inspect")
}
},
async (input) => {
const { typeName, fieldName } = input;
if (!typeName) throw new Error("Missing 'typeName' argument");
if (!fieldName) throw new Error("Missing 'fieldName' argument");
const schema = await introspectSchema(GRAPHQL_ENDPOINT);
const type = schema.types.find((t) => t.name === typeName) ||
schema.inputs.find((t) => t.name === typeName);
if (!type) {
throw new Error(`Type '${typeName}' not found`);
}
const fieldExists = type.fields.includes(fieldName);
if (!fieldExists) {
throw new Error(`Field '${fieldName}' not found in type '${typeName}'`);
}
return {
content: [{ type: "text", text: JSON.stringify({ type: typeName, field: fieldName, details: type }, null, 2) }],
};
}
);
server.registerTool(
"related",
{
title: "Related Types",
description: "Find types that are related to a specific type",
inputSchema: {
typeName: z.string().describe("Name of the GraphQL type to find relations for")
}
},
async (input) => {
const { typeName } = input;
if (!typeName) throw new Error("Missing 'typeName' argument");
const schema = await introspectSchema(GRAPHQL_ENDPOINT);
const relatedTypes = schema.types.filter(type =>
type.name.includes(typeName) ||
type.fields.some(field => field.includes(typeName))
);
return {
content: [{ type: "text", text: JSON.stringify(relatedTypes, null, 2) }],
};
}
);
await server.connect(new StdioServerTransport());
}
// Execute the main function
main().catch((err) => {
console.error("MCP server failed to start:", err);
process.exit(1);
});