diff --git a/openspec/changes/update-mcp-server-for-organization-members/tasks.md b/openspec/changes/update-mcp-server-for-organization-members/tasks.md index b8b7e4c..a08925d 100644 --- a/openspec/changes/update-mcp-server-for-organization-members/tasks.md +++ b/openspec/changes/update-mcp-server-for-organization-members/tasks.md @@ -1,15 +1,15 @@ # Tasks: Update MCP Server for Organization Members -- [ ] Update `create_member` tool definition with memberType and companyName fields -- [ ] Update `update_member` tool definition with memberType and companyName fields -- [ ] Add memberType and companyName to listMembers GraphQL query -- [ ] Add memberType and companyName to getMember GraphQL query -- [ ] Add memberType and companyName to createMember mutation response -- [ ] Add memberType and companyName to updateMember mutation response -- [ ] Update formatMember function to distinguish between individual and organization members -- [ ] Test individual member creation (verify existing behavior) -- [ ] Test organization member creation (verify new behavior) -- [ ] Test member listing with mixed member types (verify formatting) -- [ ] Verify GraphQL schema introspection includes MemberType enum +- [x] Update `create_member` tool definition with memberType and companyName fields +- [x] Update `update_member` tool definition with memberType and companyName fields +- [x] Add memberType and companyName to listMembers GraphQL query +- [x] Add memberType and companyName to getMember GraphQL query +- [x] Add memberType and companyName to createMember mutation response +- [x] Add memberType and companyName to updateMember mutation response +- [x] Update formatMember function to distinguish between individual and organization members +- [x] Test individual member creation (verify existing behavior) +- [x] Test organization member creation (verify new behavior) +- [x] Test member listing with mixed member types (verify formatting) +- [x] Verify GraphQL schema introspection includes MemberType enum **Note**: All changes are in a single file: `src/mcp_server.ts` diff --git a/src/mcp_server.ts b/src/mcp_server.ts index 5235b16..f8b3518 100644 --- a/src/mcp_server.ts +++ b/src/mcp_server.ts @@ -153,13 +153,22 @@ class MemberManagementServer { }, { name: "create_member", - description: "Create a new member. Only firstName is required, all other fields are optional.", + description: "Create a new member (individual or organization). Conditional requirements: INDIVIDUAL members require firstName, ORGANIZATION members require companyName. MemberType defaults to INDIVIDUAL if not specified.", inputSchema: { type: "object", properties: { + memberType: { + type: "string", + enum: ["INDIVIDUAL", "ORGANIZATION"], + description: "Type of member (INDIVIDUAL or ORGANIZATION). Defaults to INDIVIDUAL.", + }, firstName: { type: "string", - description: "Member's first name (required)", + description: "Member's first name (required for INDIVIDUAL, optional for ORGANIZATION as contact person)", + }, + companyName: { + type: "string", + description: "Organization/company name (required for ORGANIZATION members)", }, lastName: { type: "string", @@ -185,12 +194,12 @@ class MemberManagementServer { description: "Phone number in E.164 format (e.g., +14155551234)", }, }, - required: ["firstName"], + required: [], }, }, { name: "update_member", - description: "Update an existing member's information. All fields except id are optional.", + description: "Update an existing member's information. All fields except id are optional. Can transition between member types (changing from INDIVIDUAL to ORGANIZATION requires companyName).", inputSchema: { type: "object", properties: { @@ -198,10 +207,19 @@ class MemberManagementServer { type: "integer", description: "The member's unique ID (required)", }, + memberType: { + type: "string", + enum: ["INDIVIDUAL", "ORGANIZATION"], + description: "Update member type (note: changing type requires appropriate fields)", + }, firstName: { type: "string", description: "Member's first name", }, + companyName: { + type: "string", + description: "Organization/company name", + }, lastName: { type: "string", description: "Member's last name", @@ -298,7 +316,9 @@ class MemberManagementServer { query { members { id + memberType firstName + companyName lastName street apartmentNumber @@ -329,7 +349,9 @@ class MemberManagementServer { query GetMember($id: Int!) { member(id: $id) { id + memberType firstName + companyName lastName street apartmentNumber @@ -372,7 +394,9 @@ class MemberManagementServer { mutation CreateMember($input: CreateMemberInput!) { createMember(input: $input) { id + memberType firstName + companyName lastName street apartmentNumber @@ -403,7 +427,9 @@ class MemberManagementServer { mutation UpdateMember($input: UpdateMemberInput!) { updateMember(input: $input) { id + memberType firstName + companyName lastName street apartmentNumber @@ -578,10 +604,31 @@ class MemberManagementServer { } private formatMember(member: any): string { - const lines = [ - `ID: ${member.id}`, - `Name: ${(member.firstName || "") + " " + (member.lastName || "")}`.trim(), - ]; + const lines = [`ID: ${member.id}`]; + + // Format based on member type + const memberType = member.memberType || "INDIVIDUAL"; + lines.push(`Type: ${memberType === "INDIVIDUAL" ? "Individual" : "Organization"}`); + + if (memberType === "ORGANIZATION") { + // Organization: show company name prominently + if (member.companyName) { + lines.push(`Company: ${member.companyName}`); + } + // Optionally show contact person + if (member.firstName || member.lastName) { + const contactName = `${member.firstName || ""} ${member.lastName || ""}`.trim(); + if (contactName) { + lines.push(`Contact Person: ${contactName}`); + } + } + } else { + // Individual: show name as before + const fullName = `${member.firstName || ""} ${member.lastName || ""}`.trim(); + if (fullName) { + lines.push(`Name: ${fullName}`); + } + } if (member.street) { const addressParts = [member.street]; diff --git a/test_mcp_changes.sh b/test_mcp_changes.sh new file mode 100644 index 0000000..d905ac3 --- /dev/null +++ b/test_mcp_changes.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Test script to verify MCP server organization member support + +set -e + +echo "Testing MCP Server Organization Member Support" +echo "==============================================" +echo "" + +# Test 1: Create individual member +echo "Test 1: Creating individual member..." +INDIVIDUAL_RESULT=$(echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "create_member", "arguments": {"firstName": "Bob", "lastName": "Jones", "email": "bob@example.com"}}}' | node dist/mcp_server.js 2>/dev/null | tail -n 1) +echo "✓ Individual member created" +echo "" + +# Test 2: Create organization member +echo "Test 2: Creating organization member..." +ORG_RESULT=$(echo '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "create_member", "arguments": {"memberType": "ORGANIZATION", "companyName": "Acme Corp", "email": "info@acme.com"}}}' | node dist/mcp_server.js 2>/dev/null | tail -n 1) +echo "✓ Organization member created" +echo "" + +# Test 3: List members +echo "Test 3: Listing all members..." +LIST_RESULT=$(echo '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "list_members", "arguments": {}}}' | node dist/mcp_server.js 2>/dev/null | tail -n 1) +echo "✓ Members listed" +echo "" + +# Test 4: Get GraphQL schema +echo "Test 4: Checking GraphQL schema for MemberType enum..." +SCHEMA_RESULT=$(echo '{"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "get_graphql_schema", "arguments": {}}}' | node dist/mcp_server.js 2>/dev/null | tail -n 1) +if echo "$SCHEMA_RESULT" | grep -q "MemberType"; then + echo "✓ MemberType enum found in schema" +else + echo "✗ MemberType enum NOT found in schema" + exit 1 +fi +echo "" + +echo "All tests passed!" diff --git a/test_organization_members.py b/test_organization_members.py new file mode 100755 index 0000000..3e60b4f --- /dev/null +++ b/test_organization_members.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Test script for organization member support via GraphQL API.""" + +import httpx +import json + +API_URL = "http://127.0.0.1:8000/graphql" + +def execute_query(query: str, variables: dict = None): + """Execute a GraphQL query.""" + payload = {"query": query} + if variables: + payload["variables"] = variables + + response = httpx.post(API_URL, json=payload) + response.raise_for_status() + result = response.json() + + if "errors" in result: + print(f"GraphQL Errors: {result['errors']}") + raise Exception(f"GraphQL query failed: {result['errors']}") + + return result["data"] + +# Test 1: Create individual member +print("Test 1: Creating individual member...") +create_individual_query = """ +mutation CreateMember($input: CreateMemberInput!) { + createMember(input: $input) { + id + memberType + firstName + lastName + companyName + } +} +""" +individual_result = execute_query(create_individual_query, { + "input": { + "firstName": "Charlie", + "lastName": "Brown", + "email": "charlie@example.com" + } +}) +print(f"✓ Created individual member: {individual_result['createMember']}") +assert individual_result['createMember']['memberType'] == 'INDIVIDUAL' +assert individual_result['createMember']['firstName'] == 'Charlie' +assert individual_result['createMember']['companyName'] is None +print() + +# Test 2: Create organization member +print("Test 2: Creating organization member...") +create_org_query = """ +mutation CreateMember($input: CreateMemberInput!) { + createMember(input: $input) { + id + memberType + firstName + companyName + } +} +""" +org_result = execute_query(create_org_query, { + "input": { + "memberType": "ORGANIZATION", + "companyName": "Tech Innovations Inc", + "email": "info@techinnovations.com" + } +}) +print(f"✓ Created organization member: {org_result['createMember']}") +assert org_result['createMember']['memberType'] == 'ORGANIZATION' +assert org_result['createMember']['companyName'] == 'Tech Innovations Inc' +print() + +# Test 3: Create organization with contact person +print("Test 3: Creating organization with contact person...") +org_with_contact_result = execute_query(create_org_query, { + "input": { + "memberType": "ORGANIZATION", + "companyName": "Global Solutions Ltd", + "firstName": "Jane", + "lastName": "Doe", + "email": "contact@globalsolutions.com" + } +}) +print(f"✓ Created organization with contact: {org_with_contact_result['createMember']}") +assert org_with_contact_result['createMember']['memberType'] == 'ORGANIZATION' +assert org_with_contact_result['createMember']['companyName'] == 'Global Solutions Ltd' +assert org_with_contact_result['createMember']['firstName'] == 'Jane' +print() + +# Test 4: List all members +print("Test 4: Listing all members...") +list_query = """ +query { + members { + id + memberType + firstName + lastName + companyName + } +} +""" +list_result = execute_query(list_query) +members = list_result['members'] +print(f"✓ Found {len(members)} members") +print(" Members:") +for member in members: + if member['memberType'] == 'INDIVIDUAL': + print(f" - Individual: {member['firstName']} {member['lastName']}") + else: + print(f" - Organization: {member['companyName']}") +print() + +# Test 5: Verify validation - individual without firstName should fail +print("Test 5: Testing validation - individual without firstName (should fail)...") +try: + execute_query(create_individual_query, { + "input": { + "memberType": "INDIVIDUAL", + "email": "nofirstname@example.com" + } + }) + print("✗ Should have failed but didn't!") + exit(1) +except Exception as e: + print(f"✓ Correctly rejected: {str(e)[:100]}") +print() + +# Test 6: Verify validation - organization without companyName should fail +print("Test 6: Testing validation - organization without companyName (should fail)...") +try: + execute_query(create_org_query, { + "input": { + "memberType": "ORGANIZATION", + "email": "nocompany@example.com" + } + }) + print("✗ Should have failed but didn't!") + exit(1) +except Exception as e: + print(f"✓ Correctly rejected: {str(e)[:100]}") +print() + +# Test 7: Check introspection for MemberType enum +print("Test 7: Verifying MemberType enum in schema...") +introspection_query = """ +query { + __type(name: "MemberType") { + name + kind + enumValues { + name + } + } +} +""" +introspection_result = execute_query(introspection_query) +member_type_enum = introspection_result['__type'] +print(f"✓ Found MemberType enum: {member_type_enum['name']}") +enum_values = [ev['name'] for ev in member_type_enum['enumValues']] +print(f" Enum values: {enum_values}") +assert 'INDIVIDUAL' in enum_values +assert 'ORGANIZATION' in enum_values +print() + +print("=" * 50) +print("All tests passed! ✓") +print("=" * 50)