refactor: Convert MCP server implementation from Python to TypeScript.

This commit is contained in:
2025-12-01 20:46:50 +01:00
parent 7d96a657b3
commit 2a6584078c
12 changed files with 1868 additions and 643 deletions
-622
View File
@@ -1,622 +0,0 @@
"""MCP Server for Clubber Member Management.
This server provides AI assistants with tools to manage society members
via the Model Context Protocol (MCP), connecting to the Clubber GraphQL API.
"""
import asyncio
import json
import os
from typing import Any, Dict, List, Optional
import httpx
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import TextContent, Tool
# GraphQL introspection query to fetch the complete schema
INTROSPECTION_QUERY = """
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
types {
kind
name
description
fields(includeDeprecated: true) {
name
description
type {
kind
name
ofType {
kind
name
}
}
args {
name
description
type {
kind
name
ofType {
kind
name
}
}
}
}
inputFields {
name
description
type {
kind
name
ofType {
kind
name
}
}
}
}
}
}
"""
class GraphQLClient:
"""HTTP client for communicating with the Clubber GraphQL API."""
def __init__(self, api_url: str):
"""Initialize the GraphQL client.
Args:
api_url: The GraphQL API endpoint URL
"""
self.api_url = api_url
self.client = httpx.AsyncClient(timeout=30.0)
async def query(
self, query: str, variables: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Execute a GraphQL query.
Args:
query: The GraphQL query string
variables: Optional query variables
Returns:
The GraphQL response data
Raises:
Exception: If the query fails or returns errors
"""
payload = {"query": query}
if variables:
payload["variables"] = variables
try:
response = await self.client.post(self.api_url, json=payload)
response.raise_for_status()
result = response.json()
if "errors" in result:
error_messages = [e.get("message", str(e)) for e in result["errors"]]
raise Exception(f"GraphQL errors: {', '.join(error_messages)}")
return result.get("data", {})
except httpx.RequestError as e:
raise Exception(f"Failed to connect to GraphQL API at {self.api_url}: {e}")
async def close(self):
"""Close the HTTP client."""
await self.client.aclose()
class MemberManagementServer:
"""MCP server providing member management tools."""
def __init__(self, api_url: str):
"""Initialize the MCP server.
Args:
api_url: The GraphQL API endpoint URL
"""
self.graphql = GraphQLClient(api_url)
self.server = Server("clubber-mcp-server")
self._register_handlers()
def _register_handlers(self):
"""Register MCP protocol handlers."""
@self.server.list_tools()
async def list_tools() -> List[Tool]:
"""List available tools."""
return [
Tool(
name="list_members",
description="List all members in the society with their complete information",
inputSchema={
"type": "object",
"properties": {},
"required": [],
},
),
Tool(
name="get_member",
description="Get detailed information about a specific member by their ID",
inputSchema={
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "The member's unique ID",
}
},
"required": ["id"],
},
),
Tool(
name="create_member",
description="Create a new member. Only firstName is required, all other fields are optional.",
inputSchema={
"type": "object",
"properties": {
"firstName": {
"type": "string",
"description": "Member's first name (required)",
},
"lastName": {
"type": "string",
"description": "Member's last name",
},
"street": {
"type": "string",
"description": "Street address",
},
"apartmentNumber": {
"type": "string",
"description": "Apartment or unit number",
},
"zip": {"type": "string", "description": "Postal code"},
"city": {"type": "string", "description": "City"},
"country": {"type": "string", "description": "Country"},
"email": {
"type": "string",
"description": "Email address (validated when provided)",
},
"phone": {
"type": "string",
"description": "Phone number in E.164 format (e.g., +14155551234)",
},
},
"required": ["firstName"],
},
),
Tool(
name="update_member",
description="Update an existing member's information. All fields except id are optional.",
inputSchema={
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "The member's unique ID (required)",
},
"firstName": {
"type": "string",
"description": "Member's first name",
},
"lastName": {
"type": "string",
"description": "Member's last name",
},
"street": {
"type": "string",
"description": "Street address",
},
"apartmentNumber": {
"type": "string",
"description": "Apartment or unit number",
},
"zip": {"type": "string", "description": "Postal code"},
"city": {"type": "string", "description": "City"},
"country": {"type": "string", "description": "Country"},
"email": {"type": "string", "description": "Email address"},
"phone": {
"type": "string",
"description": "Phone number in E.164 format",
},
},
"required": ["id"],
},
),
Tool(
name="get_graphql_schema",
description="Get the complete GraphQL schema definition via introspection. This allows AI agents to discover all available types, queries, mutations, and their fields dynamically.",
inputSchema={
"type": "object",
"properties": {},
"required": [],
},
),
Tool(
name="execute_graphql_query",
description="Execute an arbitrary GraphQL query or mutation. Use this for complex queries beyond the dedicated member tools. The query can include variables for parameterization.",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The GraphQL query or mutation string",
},
"variables": {
"type": "object",
"description": "Optional variables for the query as a JSON object",
},
},
"required": ["query"],
},
),
]
@self.server.call_tool()
async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
"""Handle tool execution."""
try:
if name == "list_members":
return await self._list_members()
elif name == "get_member":
return await self._get_member(arguments["id"])
elif name == "create_member":
return await self._create_member(arguments)
elif name == "update_member":
return await self._update_member(arguments)
elif name == "get_graphql_schema":
return await self._get_graphql_schema()
elif name == "execute_graphql_query":
return await self._execute_graphql_query(
arguments["query"], arguments.get("variables")
)
else:
raise ValueError(f"Unknown tool: {name}")
except Exception as e:
return [TextContent(type="text", text=f"Error: {str(e)}")]
async def _list_members(self) -> List[TextContent]:
"""List all members."""
query = """
query {
members {
id
firstName
lastName
street
apartmentNumber
zip
city
country
email
phone
createdAt
updatedAt
}
}
"""
data = await self.graphql.query(query)
members = data.get("members", [])
return [
TextContent(
type="text",
text=f"Found {len(members)} member(s):\n\n{self._format_members(members)}",
)
]
async def _get_member(self, member_id: int) -> List[TextContent]:
"""Get a specific member by ID."""
query = """
query GetMember($id: Int!) {
member(id: $id) {
id
firstName
lastName
street
apartmentNumber
zip
city
country
email
phone
createdAt
updatedAt
}
}
"""
data = await self.graphql.query(query, {"id": member_id})
member = data.get("member")
if not member:
return [
TextContent(type="text", text=f"Member with ID {member_id} not found")
]
return [
TextContent(
type="text", text=f"Member found:\n\n{self._format_member(member)}"
)
]
async def _create_member(self, input_data: Dict[str, Any]) -> List[TextContent]:
"""Create a new member."""
mutation = """
mutation CreateMember($input: CreateMemberInput!) {
createMember(input: $input) {
id
firstName
lastName
street
apartmentNumber
zip
city
country
email
phone
createdAt
updatedAt
}
}
"""
data = await self.graphql.query(mutation, {"input": input_data})
member = data.get("createMember")
return [
TextContent(
type="text",
text=f"Member created successfully:\n\n{self._format_member(member)}",
)
]
async def _update_member(self, input_data: Dict[str, Any]) -> List[TextContent]:
"""Update an existing member."""
mutation = """
mutation UpdateMember($input: UpdateMemberInput!) {
updateMember(input: $input) {
id
firstName
lastName
street
apartmentNumber
zip
city
country
email
phone
createdAt
updatedAt
}
}
"""
data = await self.graphql.query(mutation, {"input": input_data})
member = data.get("updateMember")
if not member:
return [
TextContent(
type="text",
text=f"Failed to update member with ID {input_data.get('id')}",
)
]
return [
TextContent(
type="text",
text=f"Member updated successfully:\n\n{self._format_member(member)}",
)
]
async def _get_graphql_schema(self) -> List[TextContent]:
"""Get the complete GraphQL schema via introspection."""
try:
data = await self.graphql.query(INTROSPECTION_QUERY)
schema = data.get("__schema", {})
# Format schema as readable text
output = ["GraphQL Schema\n" + "=" * 50 + "\n"]
# Query type
if schema.get("queryType"):
output.append(f"Query Type: {schema['queryType']['name']}\n")
# Mutation type
if schema.get("mutationType"):
output.append(f"Mutation Type: {schema['mutationType']['name']}\n")
output.append("\nAvailable Types:\n" + "-" * 50)
# List all types (excluding internal types)
for type_info in schema.get("types", []):
type_name = type_info.get("name", "")
# Skip internal GraphQL types
if type_name.startswith("__"):
continue
output.append(f"\n{type_info.get('kind', 'OBJECT')}: {type_name}")
if type_info.get("description"):
output.append(f" Description: {type_info['description']}")
# List fields
fields = type_info.get("fields", [])
if fields:
output.append(" Fields:")
for field in fields:
field_type = self._format_type(field.get("type", {}))
field_desc = (
f" - {field.get('description')}"
if field.get("description")
else ""
)
output.append(
f" - {field['name']}: {field_type}{field_desc}"
)
# List field arguments
args = field.get("args", [])
if args:
output.append(" Arguments:")
for arg in args:
arg_type = self._format_type(arg.get("type", {}))
arg_desc = (
f" - {arg.get('description')}"
if arg.get("description")
else ""
)
output.append(
f" - {arg['name']}: {arg_type}{arg_desc}"
)
# List input fields
input_fields = type_info.get("inputFields", [])
if input_fields:
output.append(" Input Fields:")
for field in input_fields:
field_type = self._format_type(field.get("type", {}))
field_desc = (
f" - {field.get('description')}"
if field.get("description")
else ""
)
output.append(
f" - {field['name']}: {field_type}{field_desc}"
)
return [TextContent(type="text", text="\n".join(output))]
except Exception as e:
error_msg = str(e)
if "Failed to connect" in error_msg:
return [
TextContent(
type="text",
text=f"Error: Could not connect to GraphQL API.\n\n"
f"Please ensure the API is running at {self.graphql.api_url}\n"
f"Start it with: uv run uvicorn src.main:app --host 127.0.0.1 --port 8000",
)
]
raise
async def _execute_graphql_query(
self, query: str, variables: Optional[Dict[str, Any]] = None
) -> List[TextContent]:
"""Execute an arbitrary GraphQL query or mutation."""
try:
data = await self.graphql.query(query, variables)
# Format the result as pretty JSON
result_json = json.dumps(data, indent=2)
return [
TextContent(
type="text",
text=f"Query executed successfully:\n\n{result_json}",
)
]
except Exception as e:
error_msg = str(e)
if "GraphQL errors:" in error_msg:
return [
TextContent(
type="text",
text=f"GraphQL validation error:\n\n{error_msg}\n\n"
f"Please check your query syntax and field names.",
)
]
elif "Failed to connect" in error_msg:
return [
TextContent(
type="text",
text=f"Error: Could not connect to GraphQL API.\n\n"
f"Please ensure the API is running at {self.graphql.api_url}",
)
]
raise
def _format_type(self, type_info: Dict[str, Any]) -> str:
"""Format a GraphQL type for display."""
kind = type_info.get("kind", "")
name = type_info.get("name", "")
if kind == "NON_NULL":
inner = self._format_type(type_info.get("ofType", {}))
return f"{inner}!"
elif kind == "LIST":
inner = self._format_type(type_info.get("ofType", {}))
return f"[{inner}]"
else:
return name or "Unknown"
def _format_member(self, member: Dict[str, Any]) -> str:
"""Format a single member for display."""
lines = [
f"ID: {member.get('id')}",
f"Name: {member.get('firstName', '')} {member.get('lastName', '')}".strip(),
]
if member.get("street"):
address_parts = [member.get("street")]
if member.get("apartmentNumber"):
address_parts.append(f"Apt {member.get('apartmentNumber')}")
lines.append(f"Street: {' '.join(address_parts)}")
if member.get("zip") or member.get("city"):
location = f"{member.get('zip', '')} {member.get('city', '')}".strip()
lines.append(f"Location: {location}")
if member.get("country"):
lines.append(f"Country: {member.get('country')}")
if member.get("email"):
lines.append(f"Email: {member.get('email')}")
if member.get("phone"):
lines.append(f"Phone: {member.get('phone')}")
if member.get("createdAt"):
lines.append(f"Created: {member.get('createdAt')}")
if member.get("updatedAt"):
lines.append(f"Updated: {member.get('updatedAt')}")
return "\n".join(lines)
def _format_members(self, members: List[Dict[str, Any]]) -> str:
"""Format multiple members for display."""
if not members:
return "No members found."
return "\n\n---\n\n".join(self._format_member(m) for m in members)
async def run(self):
"""Run the MCP server."""
async with stdio_server() as (read_stream, write_stream):
await self.server.run(
read_stream,
write_stream,
self.server.create_initialization_options(),
)
async def main():
"""Main entry point."""
# Get API URL from environment or use default
api_url = os.getenv("CLUBBER_API_URL", "http://127.0.0.1:8000/graphql")
print("Starting Clubber MCP Server...", flush=True)
print(f"GraphQL API URL: {api_url}", flush=True)
server = MemberManagementServer(api_url)
try:
await server.run()
finally:
await server.graphql.close()
if __name__ == "__main__":
asyncio.run(main())
+649
View File
@@ -0,0 +1,649 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
// GraphQL introspection query
const INTROSPECTION_QUERY = `
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
types {
kind
name
description
fields(includeDeprecated: true) {
name
description
type {
kind
name
ofType {
kind
name
}
}
args {
name
description
type {
kind
name
ofType {
kind
name
}
}
}
}
inputFields {
name
description
type {
kind
name
ofType {
kind
name
}
}
}
}
}
}
`;
class GraphQLClient {
private apiUrl: string;
constructor(apiUrl: string) {
this.apiUrl = apiUrl;
}
getApiUrl(): string {
return this.apiUrl;
}
async query(query: string, variables?: Record<string, any>): Promise<any> {
const payload: any = { query };
if (variables) {
payload.variables = variables;
}
try {
const response = await fetch(this.apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = (await response.json()) as any;
if (result.errors) {
const errorMessages = result.errors.map((e: any) => e.message || String(e));
throw new Error(`GraphQL errors: ${errorMessages.join(", ")}`);
}
return result.data || {};
} catch (error: any) {
throw new Error(`Failed to connect to GraphQL API at ${this.apiUrl}: ${error.message}`);
}
}
}
class MemberManagementServer {
private graphql: GraphQLClient;
private server: Server;
constructor(apiUrl: string) {
this.graphql = new GraphQLClient(apiUrl);
this.server = new Server(
{
name: "clubber-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.registerHandlers();
}
private registerHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_members",
description: "List all members in the society with their complete information",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "get_member",
description: "Get detailed information about a specific member by their ID",
inputSchema: {
type: "object",
properties: {
id: {
type: "integer",
description: "The member's unique ID",
},
},
required: ["id"],
},
},
{
name: "create_member",
description: "Create a new member. Only firstName is required, all other fields are optional.",
inputSchema: {
type: "object",
properties: {
firstName: {
type: "string",
description: "Member's first name (required)",
},
lastName: {
type: "string",
description: "Member's last name",
},
street: {
type: "string",
description: "Street address",
},
apartmentNumber: {
type: "string",
description: "Apartment or unit number",
},
zip: { type: "string", description: "Postal code" },
city: { type: "string", description: "City" },
country: { type: "string", description: "Country" },
email: {
type: "string",
description: "Email address (validated when provided)",
},
phone: {
type: "string",
description: "Phone number in E.164 format (e.g., +14155551234)",
},
},
required: ["firstName"],
},
},
{
name: "update_member",
description: "Update an existing member's information. All fields except id are optional.",
inputSchema: {
type: "object",
properties: {
id: {
type: "integer",
description: "The member's unique ID (required)",
},
firstName: {
type: "string",
description: "Member's first name",
},
lastName: {
type: "string",
description: "Member's last name",
},
street: {
type: "string",
description: "Street address",
},
apartmentNumber: {
type: "string",
description: "Apartment or unit number",
},
zip: { type: "string", description: "Postal code" },
city: { type: "string", description: "City" },
country: { type: "string", description: "Country" },
email: { type: "string", description: "Email address" },
phone: {
type: "string",
description: "Phone number in E.164 format",
},
},
required: ["id"],
},
},
{
name: "get_graphql_schema",
description:
"Get the complete GraphQL schema definition via introspection. This allows AI agents to discover all available types, queries, mutations, and their fields dynamically.",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "execute_graphql_query",
description:
"Execute an arbitrary GraphQL query or mutation. Use this for complex queries beyond the dedicated member tools. The query can include variables for parameterization.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "The GraphQL query or mutation string",
},
variables: {
type: "object",
description: "Optional variables for the query as a JSON object",
},
},
required: ["query"],
},
},
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "list_members":
return await this.listMembers();
case "get_member":
return await this.getMember(args as any);
case "create_member":
return await this.createMember(args as any);
case "update_member":
return await this.updateMember(args as any);
case "get_graphql_schema":
return await this.getGraphqlSchema();
case "execute_graphql_query":
return await this.executeGraphqlQuery(args as any);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
}
private async listMembers() {
const query = `
query {
members {
id
firstName
lastName
street
apartmentNumber
zip
city
country
email
phone
createdAt
updatedAt
}
}
`;
const data = await this.graphql.query(query);
const members = data.members || [];
return {
content: [
{
type: "text",
text: `Found ${members.length} member(s):\n\n${this.formatMembers(members)}`,
},
],
};
}
private async getMember(args: { id: number }) {
const query = `
query GetMember($id: Int!) {
member(id: $id) {
id
firstName
lastName
street
apartmentNumber
zip
city
country
email
phone
createdAt
updatedAt
}
}
`;
const data = await this.graphql.query(query, { id: args.id });
const member = data.member;
if (!member) {
return {
content: [
{
type: "text",
text: `Member with ID ${args.id} not found`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Member found:\n\n${this.formatMember(member)}`,
},
],
};
}
private async createMember(inputData: any) {
const mutation = `
mutation CreateMember($input: CreateMemberInput!) {
createMember(input: $input) {
id
firstName
lastName
street
apartmentNumber
zip
city
country
email
phone
createdAt
updatedAt
}
}
`;
const data = await this.graphql.query(mutation, { input: inputData });
const member = data.createMember;
return {
content: [
{
type: "text",
text: `Member created successfully:\n\n${this.formatMember(member)}`,
},
],
};
}
private async updateMember(inputData: any) {
const mutation = `
mutation UpdateMember($input: UpdateMemberInput!) {
updateMember(input: $input) {
id
firstName
lastName
street
apartmentNumber
zip
city
country
email
phone
createdAt
updatedAt
}
}
`;
const data = await this.graphql.query(mutation, { input: inputData });
const member = data.updateMember;
if (!member) {
return {
content: [
{
type: "text",
text: `Failed to update member with ID ${inputData.id}`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Member updated successfully:\n\n${this.formatMember(member)}`,
},
],
};
}
private async getGraphqlSchema() {
try {
const data = await this.graphql.query(INTROSPECTION_QUERY);
const schema = data.__schema || {};
const output = ["GraphQL Schema\n" + "=".repeat(50) + "\n"];
if (schema.queryType) {
output.push(`Query Type: ${schema.queryType.name}\n`);
}
if (schema.mutationType) {
output.push(`Mutation Type: ${schema.mutationType.name}\n`);
}
output.push("\nAvailable Types:\n" + "-".repeat(50));
for (const typeInfo of schema.types || []) {
const typeName = typeInfo.name || "";
if (typeName.startsWith("__")) continue;
output.push(`\n${typeInfo.kind || "OBJECT"}: ${typeName}`);
if (typeInfo.description) {
output.push(` Description: ${typeInfo.description}`);
}
const fields = typeInfo.fields || [];
if (fields.length > 0) {
output.push(" Fields:");
for (const field of fields) {
const fieldType = this.formatType(field.type || {});
const fieldDesc = field.description ? ` - ${field.description}` : "";
output.push(` - ${field.name}: ${fieldType}${fieldDesc}`);
const args = field.args || [];
if (args.length > 0) {
output.push(" Arguments:");
for (const arg of args) {
const argType = this.formatType(arg.type || {});
const argDesc = arg.description ? ` - ${arg.description}` : "";
output.push(` - ${arg.name}: ${argType}${argDesc}`);
}
}
}
}
const inputFields = typeInfo.inputFields || [];
if (inputFields.length > 0) {
output.push(" Input Fields:");
for (const field of inputFields) {
const fieldType = this.formatType(field.type || {});
const fieldDesc = field.description ? ` - ${field.description}` : "";
output.push(` - ${field.name}: ${fieldType}${fieldDesc}`);
}
}
}
return {
content: [
{
type: "text",
text: output.join("\n"),
},
],
};
} catch (e: any) {
const errorMsg = String(e);
if (errorMsg.includes("Failed to connect")) {
return {
content: [
{
type: "text",
text: `Error: Could not connect to GraphQL API.\n\nPlease ensure the API is running at ${this.graphql.getApiUrl()}\nStart it with: uv run uvicorn src.main:app --host 127.0.0.1 --port 8000`,
},
],
isError: true,
};
}
throw e;
}
}
private async executeGraphqlQuery(args: { query: string; variables?: Record<string, any> }) {
try {
const data = await this.graphql.query(args.query, args.variables);
return {
content: [
{
type: "text",
text: `Query executed successfully:\n\n${JSON.stringify(data, null, 2)}`,
},
],
};
} catch (e: any) {
const errorMsg = String(e);
if (errorMsg.includes("GraphQL errors:")) {
return {
content: [
{
type: "text",
text: `GraphQL validation error:\n\n${errorMsg}\n\nPlease check your query syntax and field names.`,
},
],
isError: true,
};
} else if (errorMsg.includes("Failed to connect")) {
return {
content: [
{
type: "text",
text: `Error: Could not connect to GraphQL API.\n\nPlease ensure the API is running at ${this.graphql.getApiUrl()}`,
},
],
isError: true,
};
}
throw e;
}
}
private formatType(typeInfo: any): string {
const kind = typeInfo.kind || "";
const name = typeInfo.name || "";
if (kind === "NON_NULL") {
const inner = this.formatType(typeInfo.ofType || {});
return `${inner}!`;
} else if (kind === "LIST") {
const inner = this.formatType(typeInfo.ofType || {});
return `[${inner}]`;
} else {
return name || "Unknown";
}
}
private formatMember(member: any): string {
const lines = [
`ID: ${member.id}`,
`Name: ${(member.firstName || "") + " " + (member.lastName || "")}`.trim(),
];
if (member.street) {
const addressParts = [member.street];
if (member.apartmentNumber) {
addressParts.push(`Apt ${member.apartmentNumber}`);
}
lines.push(`Street: ${addressParts.join(" ")}`);
}
if (member.zip || member.city) {
const location = `${member.zip || ""} ${member.city || ""}`.trim();
lines.push(`Location: ${location}`);
}
if (member.country) {
lines.push(`Country: ${member.country}`);
}
if (member.email) {
lines.push(`Email: ${member.email}`);
}
if (member.phone) {
lines.push(`Phone: ${member.phone}`);
}
if (member.createdAt) {
lines.push(`Created: ${member.createdAt}`);
}
if (member.updatedAt) {
lines.push(`Updated: ${member.updatedAt}`);
}
return lines.join("\n");
}
private formatMembers(members: any[]): string {
if (!members || members.length === 0) {
return "No members found.";
}
return members.map((m) => this.formatMember(m)).join("\n\n---\n\n");
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Clubber MCP Server running on stdio");
}
}
async function main() {
const apiUrl = process.env.CLUBBER_API_URL || "http://127.0.0.1:8000/graphql";
console.error("Starting Clubber MCP Server...");
console.error(`GraphQL API URL: ${apiUrl}`);
const server = new MemberManagementServer(apiUrl);
await server.run();
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});