MCP Publishing Issues
Comprehensive debugging guide for Model Context Protocol service publication.
Common Issues & Solutions
Issue 1: Service Not Appearing in Directory
Symptom: Published service not visible in GET /api/v1/mcp/services
Cause 1A: Publication Failed Silently
# Check service creation
curl -X POST http://localhost:8080/api/v1/mcp/services \
-H "Content-Type: application/json" \
-d '{
"name": "MyService",
"category": "UTILITY",
"sourceType": "REST_ENDPOINT",
"restEndpoint": "/v1/orders"
}'
# Look for 201 Created response
# If 400/500, check error message in response body
Solution:
// Backend: Enable debug logging
logging:
level:
com.valkyrlabs.mcp: DEBUG
// Check service validation
@Service
public class MCPService {
public void publishService(PublishServiceRequest req) throws ValidationException {
if (req.getName() == null || req.getName().isBlank()) {
throw new ValidationException("Name cannot be empty");
}
if (req.getSourceType() == null) {
throw new ValidationException("sourceType required");
}
// Continue validation...
}
}
Cause 1B: Category Mismatch
// Valid categories (enum)
UTILITY, ANALYTICS, INTEGRATION, COMMUNICATION,
AUTOMATION, SECURITY, DATA_MANAGEMENT, AI_ML
Solution: Verify category matches enum
# Wrong: "UTILS"
{"category": "UTILS"} # ❌ ValidationException
# Correct: "UTILITY"
{"category": "UTILITY"} # ✅ Success
Cause 1C: Source Not Accessible
Symptom: Service publishes but endpoints return 404
# Test REST endpoint directly
curl http://localhost:8080/v1/orders -v
# If 404, endpoint doesn't exist
# If 403, authentication missing
# If 500, internal error
Solution:
// Ensure endpoint is registered
@RestController
@RequestMapping("/v1/orders")
public class OrderController {
@GetMapping
@PreAuthorize("hasRole('USER')") // Ensure auth allows public access
public ResponseEntity<List<Order>> getOrders() {
return ResponseEntity.ok(orderService.findAll());
}
}
// If endpoint requires auth, add to MCP service config
@Component
public class MCPAuthInterceptor {
@Bean
public ClientHttpRequestInterceptor mcpAuthInterceptor() {
return (request, body, execution) -> {
// Add service account credentials
request.getHeaders().setBearerAuth(getServiceToken());
return execution.execute(request, body);
};
}
}
Issue 2: Service Discovery Returns Empty/Wrong Metadata
Symptom: Service appears but missing endpoints or schema information
Cause 2A: Schema Not Inferred
Problem: MCP can't automatically determine request/response schemas from REST endpoints
# Discovery call returns empty schemas
GET /api/v1/mcp/services/discover?url=http://localhost:8080/v1/orders
# Response: endpoints with no schema
{
"endpoints": [
{
"path": "/v1/orders",
"method": "GET",
"schema": null # ❌ Missing schema
}
]
}
Solution: Provide explicit schema
// Option 1: Add OpenAPI/Swagger annotations
@RestController
@RequestMapping("/v1/orders")
public class OrderController {
@GetMapping
@Operation(summary = "List all orders")
@ApiResponse(responseCode = "200", description = "Orders retrieved")
public ResponseEntity<List<Order>> getOrders() {
return ResponseEntity.ok(orderService.findAll());
}
}
// Option 2: Register custom schema in MCP service
POST /api/v1/mcp/services/{id}/schema
{
"endpoints": [
{
"path": "/v1/orders",
"method": "GET",
"requestSchema": { ... },
"responseSchema": {
"type": "array",
"items": { "$ref": "#/components/schemas/Order" }
}
}
]
}
Cause 2B: Workflow Publication Lost Metadata
Problem: Workflow published but input/output fields not visible
// When publishing workflow, capture variables
@Transactional
public void publishWorkflow(UUID workflowId, PublishServiceRequest req) {
Workflow workflow = workflowRepo.findById(workflowId).orElseThrow();
// Extract input variables from first task
Task firstTask = workflow.getTasks().get(0);
Map<String, Object> inputVars = extractVariables(firstTask.getInputMapping());
// Extract output variables from last task
Task lastTask = workflow.getTasks().get(workflow.getTasks().size() - 1);
Map<String, Object> outputVars = extractVariables(lastTask.getOutputMapping());
// Store in service metadata
MCPService service = new MCPService();
service.setInputSchema(buildSchema(inputVars));
service.setOutputSchema(buildSchema(outputVars));
mcpServiceRepo.save(service);
}
Issue 3: Service Invocation Fails with 500 Error
Symptom: Service invocation returns Internal Server Error
Cause 3A: Endpoint Authentication Required
# Service call returns 401 Unauthorized
curl -X POST http://localhost:8080/api/v1/mcp/invoke/myservice \
-H "Content-Type: application/json" \
-d '{"orderId": "123"}'
# Error: 401 Unauthorized
Solution: Add service credentials
// Configure service account
@Configuration
public class MCPServiceConfig {
@Bean
public RestTemplate mcpRestTemplate() {
RestTemplate template = new RestTemplate();
// Add auth interceptor
template.getInterceptors().add((request, body, execution) -> {
String token = getServiceAccountToken();
request.getHeaders().setBearerAuth(token);
return execution.execute(request, body);
});
return template;
}
private String getServiceAccountToken() {
// Get JWT for service account
return tokenService.generateToken("mcp-service-account");
}
}
Cause 3B: Input Validation Failed
# Service call with invalid input
curl -X POST http://localhost:8080/api/v1/mcp/invoke/myservice \
-H "Content-Type: application/json" \
-d '{"orderId": "not-a-uuid"}'
# Error: 400 Bad Request - "orderId must be UUID format"
Solution: Validate inputs before invocation
@Transactional
public Object invokeService(String serviceId, Map<String, Object> input) throws ValidationException {
MCPService service = mcpServiceRepo.findById(serviceId).orElseThrow();
// Validate input against schema
JsonSchema schema = parseSchema(service.getInputSchema());
ValidationResult result = validateAgainstSchema(input, schema);
if (!result.isValid()) {
throw new ValidationException("Input validation failed: " + result.getErrors());
}
// Safe to invoke
return invokeBackend(service, input);
}
Cause 3C: Workflow Task Failed
Problem: Workflow was published but task fails during execution
# Check workflow execution logs
curl http://localhost:8080/v1/workflow/{workflowId}/tasks/{taskId}
# Response shows task failure details
{
"id": "task-123",
"status": "FAILED",
"error": "ExecModule 'emailModule' output missing required field: 'recipient'"
}
Solution: Fix workflow task
# Fix output mapping
workflows:
- id: send-order-email
tasks:
- id: send-email
moduleId: emailModule
inputMapping:
to: workflow.state.customerEmail
subject: "Order Confirmation"
outputMapping:
# ✅ Map output field that module actually returns
sentAt: workflow.state.emailSentAt
emailId: workflow.state.lastEmailId
Issue 4: Service Performance Degradation
Symptom: Service was fast, now returns 504 Timeout
Cause 4A: Backend Endpoint Slow
# Measure endpoint directly
time curl http://localhost:8080/v1/orders
# If > 30 seconds, backend is slow
Solution: Optimize backend
// Problem: N+1 queries
@GetMapping
public ResponseEntity<List<Order>> getOrders() {
// ❌ SLOW: 1 query per order for customer data
List<Order> orders = orderRepo.findAll();
return ResponseEntity.ok(orders);
}
// Solution: Eager load relationships
@GetMapping
public ResponseEntity<List<Order>> getOrders() {
// ✅ FAST: Single query with join
@Query("""
SELECT DISTINCT o FROM Order o
LEFT JOIN FETCH o.customer
LEFT JOIN FETCH o.items
""")
List<Order> orders = orderRepo.findAllWithDetails();
return ResponseEntity.ok(orders);
}
Cause 4B: Too Many Service Invocations
Problem: Service invoked repeatedly in loop without caching
// Frontend: Invoking service 100 times
for (let orderId of orderIds) {
const result = await mcpService.invoke("getOrderDetails", { orderId });
results.push(result);
}
// ❌ 100 separate HTTP requests
Solution: Batch operations
// Backend: Add batch endpoint
@PostMapping("/batch")
public ResponseEntity<List<Order>> getOrdersBatch(
@RequestBody List<String> orderIds
) {
List<Order> orders = orderRepo.findAllById(orderIds);
return ResponseEntity.ok(orders);
}
// Frontend: Use batch
const results = await mcpService.invoke('getOrdersBatch', {
orderIds: [/* 100 IDs */]
});
// ✅ 1 HTTP request
Cause 4C: Caching Not Configured
// Add response caching
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("orders", "users");
}
}
// Cache query results
@Service
public class OrderService {
@Cacheable(value = "orders", key = "#id")
public Order getOrder(String id) {
return orderRepo.findById(id).orElseThrow();
}
// Invalidate cache on update
@CacheEvict(value = "orders", key = "#order.id")
public Order updateOrder(Order order) {
return orderRepo.save(order);
}
}
Issue 5: Service Access Denied (403)
Symptom: Service callable by admin, denied for other users
Cause 5A: Service-Level Authorization
# Call returns 403 Forbidden
curl -X POST http://localhost:8080/api/v1/mcp/invoke/private-service \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{...}'
# Error: Access Denied
Solution: Configure service permissions
// Grant role access
@Transactional
public void grantServiceAccess(String serviceId, String roleName) {
MCPService service = mcpServiceRepo.findById(serviceId).orElseThrow();
// Add role requirement
service.setRequiredRoles(List.of("ROLE_" + roleName));
mcpServiceRepo.save(service);
}
// Check permission on invocation
@PreAuthorize("hasAnyRole(@mcpService.getRequiredRoles())")
@PostMapping("/invoke/{serviceId}")
public ResponseEntity<Object> invokeService(@PathVariable String serviceId) {
// ...
}
Cause 5B: Endpoint-Level Authorization
Problem: Service visible but endpoint requires specific role
// Endpoint requires ADMIN role
@RestController
@RequestMapping("/v1/admin/orders")
public class AdminOrderController {
@GetMapping
@PreAuthorize("hasRole('ADMIN')") // Only admins
public ResponseEntity<List<Order>> getOrders() {
return ResponseEntity.ok(orderService.findAll());
}
}
Solution: Publish separate service or relax permissions
// Option 1: Create public endpoint
@RestController
@RequestMapping("/v1/orders")
public class OrderController {
@GetMapping
@PreAuthorize("hasRole('USER')") // All users
public ResponseEntity<List<Order>> getOrders() {
return ResponseEntity.ok(orderService.findAll());
}
}
// Option 2: Add service account role
@Configuration
public class SecurityConfig {
@Bean
public GrantedAuthority mcpServiceRole() {
return new SimpleGrantedAuthority("ROLE_MCP_SERVICE");
}
}
// Option 3: Update endpoint auth
@GetMapping
@PreAuthorize("hasAnyRole('ADMIN', 'MCP_SERVICE')")
public ResponseEntity<List<Order>> getOrders() {
// Now both can access
}
Issue 6: Workflow Execution Stalls After MCP Service Call
Symptom: Workflow pauses when invoking MCP service, never resumes
Cause 6A: Service Response Not Returned
// Async service doesn't return response
@Override
public Map<String, Object> execute(...) {
CompletableFuture.runAsync(() -> {
// Do work but never signal completion
doAsyncWork();
// ❌ Missing return or callback
});
return null; // ❌ Returns null, workflow stalls
}
Solution: Return response immediately
@Override
public Map<String, Object> execute(...) {
// Return immediately
CompletableFuture.runAsync(() -> {
try {
Object result = doAsyncWork();
// Store result or callback
updateWorkflowState(result);
} catch (Exception e) {
logger.error("Async work failed", e);
}
});
// ✅ Return immediately
return Map.of("status", "queued", "message", "Processing...");
}
Cause 6B: Service Call Timeout
# Check timeout configuration
# Default: 30 seconds
# If service takes > 30s, workflow times out
# Measure endpoint
time curl -X POST http://localhost:8080/api/v1/mcp/invoke/slow-service \
-d '{...}' -w "\nTotal time: %{time_total}s\n"
Solution: Increase timeout or optimize
// Increase timeout
@Configuration
public class MCPConfig {
@Bean
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(60000); // 60 seconds
factory.setReadTimeout(120000); // 2 minutes
return new RestTemplate(factory);
}
}
// Or use async invocation
@PostMapping("/invoke-async/{serviceId}")
public ResponseEntity<Map<String, String>> invokeServiceAsync(
@PathVariable String serviceId,
@RequestBody Map<String, Object> input
) {
String jobId = UUID.randomUUID().toString();
CompletableFuture.runAsync(() -> {
Object result = mcpService.invoke(serviceId, input);
storeResult(jobId, result);
});
return ResponseEntity.accepted().body(Map.of("jobId", jobId));
}
Issue 7: Service Metadata Out of Sync with Reality
Symptom: Service documentation says endpoint takes orderId, but it actually takes id
Cause 7A: Manual Schema Not Updated
Problem: Schema registered manually but endpoint changed
// Schema says: { "orderId": "string" }
// But endpoint expects: { "id": "string" }
POST /api/v1/mcp/invoke/getOrder
{ "orderId": "123" }
// Error: "id is required"
Solution: Re-discover and update schema
# Re-discover endpoint
POST /api/v1/mcp/services/{id}/rediscover
# Or manually update
PUT /api/v1/mcp/services/{id}/schema
{
"inputSchema": {
"type": "object",
"properties": {
"id": { "type": "string" } # ✅ Updated
},
"required": ["id"]
}
}
Cause 7B: OpenAPI Spec Stale
Problem: Spec in OpenAPI file doesn't match actual endpoint
# openapi/core.yaml
paths:
/v1/orders/{id}:
get:
parameters:
- name: id
schema:
type: string
format: uuid
// But actual endpoint
@GetMapping("/v1/orders/{orderId}") // ❌ Parameter name different!
public Order getOrder(@PathVariable String orderId) { }
Solution: Update OpenAPI spec
# Fix spec
paths:
/v1/orders/{orderId}: # ✅ Match actual endpoint
get:
parameters:
- name: orderId
schema:
type: string
format: uuid
Then regenerate:
cd thorapi
mvn generate-resources
Debugging Checklist
✅ Service Publication
- Service name non-empty
- Category valid (UTILITY, ANALYTICS, etc.)
- Source endpoint accessible (curl test)
- Authentication configured if needed
- Response contains 201 Created
✅ Service Discovery
- Service appears in
GET /api/v1/mcp/services - Metadata includes description
- Input/output schemas valid JSON
- Categories match enum
- No null fields
✅ Service Invocation
- Input validates against schema
- Service account has permission
- Endpoint accessible with service credentials
- Request completes within timeout
- Response valid JSON
✅ Workflow Integration
- Service returns response map
- Output fields mapped correctly
- No missing required fields
- Workflow doesn't stall after invocation
- Task marked as completed
✅ Performance
- Endpoint response < 10 seconds
- No N+1 queries in backend
- Batch operations where possible
- Caching configured for read-heavy endpoints
- No timeout errors in logs
Quick Reference: Testing Commands
Test Service Publication
# Publish REST endpoint
curl -X POST http://localhost:8080/api/v1/mcp/services \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "OrderService",
"category": "INTEGRATION",
"description": "Order management",
"sourceType": "REST_ENDPOINT",
"restEndpoint": "/v1/orders"
}'
List Services
curl http://localhost:8080/api/v1/mcp/services
Invoke Service
curl -X POST http://localhost:8080/api/v1/mcp/invoke/OrderService \
-H "Content-Type: application/json" \
-d '{"orderId": "123"}'
Check Logs
# Enable debug logging
tail -f logs/application.log | grep "mcp"
# Or in code
logging:
level:
com.valkyrlabs.mcp: DEBUG
com.valkyrlabs.workflow: DEBUG
✅ TROUBLESHOOTING GUIDE COMPLETE