A production-ready Spring Boot SDK for building AI-powered automation systems that respond to webhooks, process events, and execute intelligent workflows. The Agent SDK provides a robust framework for integrating AI agents with enterprise tools like Jira, Snyk, GitHub, and custom services through the Model Context Protocol (MCP).
- Customers: Use and extend the
appmodule to add your webhook controllers, handlers, schedulers, and agent workflows. See Quick Start for running locally or with Docker. - Contributors: Enhance the framework in
internal-core(messaging, MCP integration, metrics, health). See CONTRIBUTING.md for development guidelines.
Agent SDK is an enterprise-grade framework that bridges the gap between external events (webhooks, scheduled tasks) and AI-powered automation. It provides:
- Event-Driven Architecture: Receive webhooks from external services and transform them into AI agent tasks
- AI Agent Orchestration: Execute sophisticated AI workflows using Claude, GPT-4, or other LLMs
- MCP Integration: Connect to any tool or service through the Model Context Protocol
- Enterprise Messaging: Built-in ActiveMQ support for reliable, scalable event processing
- Production Monitoring: Comprehensive health checks, Prometheus metrics, and observability
- Extensible Framework: Clear separation between framework code and customer customizations
- Installation
- Usage
- Key Concepts
- Use Cases
- Features
- Architecture Overview
- Module Structure
- Getting Started
- Health Checks & Monitoring
- Developer Guide
- Messaging System
- Technology Stack
repositories {
mavenCentral()
mavenLocal()
maven {
url = uri("https://maven.pkg.github.com/David-Parry/spring-command-sdk")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
dependencies {
implementation("ai.qodo.command:internal-core:2.0.0")
}<dependency>
<groupId>ai.qodo.command</groupId>
<artifactId>internal-core</artifactId>
<version>2.0.0</version>
</dependency>The library auto-configures when added to a Spring Boot application. Simply add the dependency and Spring Boot will automatically discover and register all components.
For a complete example application, see qodo-app.
The SDK transforms external events (webhooks, scheduled tasks, API calls) into AI agent tasks. Each event flows through a pipeline: Controller → Message Queue → Agent Engine → Handler.
MCP is an open standard for connecting AI models to external tools and data sources. Agent SDK uses MCP to give AI agents access to:
- File systems and code repositories
- APIs and databases
- Build tools and test runners
- Custom enterprise tools
Agents are AI-powered workflows defined in YAML that:
- Receive structured input from events
- Execute tasks using MCP tools
- Return structured output
- Trigger follow-up actions via handlers
Handlers are post-processing components that execute after an agent completes. They can:
- Transform and publish results
- Trigger additional agents
- Send notifications
- Update external systems
- Automated Code Reviews: Analyze pull requests and provide feedback
- Bug Fixing: Automatically fix bugs based on Jira tickets
- Security Remediation: Fix vulnerabilities detected by Snyk/SonarQube
- CI/CD Pipeline Management: Respond to build failures, deploy applications
- Incident Response: Automatically investigate and remediate alerts
- Infrastructure Management: Provision resources based on requests
- Monitoring & Alerting: Intelligent alert correlation and response
- Compliance Automation: Ensure systems meet regulatory requirements
- Customer Support: Process support tickets with AI assistance
- Document Processing: Extract and process information from documents
- Workflow Orchestration: Chain multiple AI agents for complex workflows
- Data Pipeline Management: Intelligent data processing and transformation
- Vulnerability Management: Prioritize and remediate security issues
- Threat Response: Automated investigation of security events
- Compliance Monitoring: Continuous compliance checking
- Access Management: Intelligent access request processing
✅ Webhook Integration: Receive and validate webhooks from external services (Snyk, Jira)
✅ AI Agent Orchestration: Execute AI-powered workflows with configurable agents
✅ Message Queue Integration: ActiveMQ-based event processing with transaction support
✅ Handler Pattern: Post-processing handlers for agent completion workflows
✅ MCP Client Integration: Model Context Protocol client support for tool execution
✅ Spring Boot Best Practices: Auto-configuration, health checks, virtual threads
✅ Extensible Architecture: Easy to add controllers, handlers, schedulers, and agents
┌─────────────────┐
│ Webhook/API │ ← External triggers (Snyk, Jira, Custom)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Controllers │ ← Validate, transform, publish to queue
└────────┬────────┘
│
▼
┌─────────────────┐
│ Message Queue │ ← ActiveMQ (event, response, audit topics)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Agent Engine │ ← Execute AI workflows (defined in agent.yml)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Handlers │ ← Post-process results, trigger next steps
└─────────────────┘
Key Packages:
ai.qodo.command.app.controllers- Webhook endpoints and REST APIs (customer-editable)ai.qodo.command.app.handlers- Post-agent completion handlers (customer-editable)ai.qodo.command.internal.service- Core services (messaging, sessions, agents)ai.qodo.command.internal.config- Spring configuration classes
The project is organized into two main modules with distinct purposes:
Location: /app/src/main/java/ai/qodo/command/app/
This module contains customer-facing code that should be modified to add custom functionality:
- Controllers (
controllers/) - Add webhook endpoints for external services - Handlers (
handlers/) - Implement post-agent completion logic - Configuration (
config/) - Add custom Spring configurations - Resources (
resources/) - Application configuration files
Examples of what to add here:
- New webhook controllers (GitHub, GitLab, custom services)
- Custom agent handlers for post-processing
- Service-specific validators
- Custom Spring beans and configurations
Location: /internal-core/src/main/java/ai/qodo/command/internal/
This module contains the core framework code that should NOT be modified by customers:
- Services (
service/) - Core messaging, WebSocket, agent orchestration - API Models (
api/) - Request/response data structures - MCP Integration (
mcp/) - Model Context Protocol client management - Metrics (
metrics/) - Prometheus metrics for MCP and WebSocket - Actuators (
actuator/) - Custom health indicators - Configuration (
config/) - Core Spring Boot auto-configuration
internal-core module may break core functionality and is not supported. All customizations should be made in the app module.
Refer to GETTING_STARTED.md for step-by-step instructions to:
- Run locally with the internal in-memory queue or with ActiveMQ via Docker
- Configure and wire agents via agent.yml
- Implement handlers and control chaining/termination
- Trigger flows from events (e.g., webhooks)
- Environment variables and troubleshooting tips
Direct link: GETTING_STARTED.md
For containerized deployment with ActiveMQ, Prometheus, and Grafana, see the Docker Compose stack in docker/docker-compose.yml. The full end-to-end steps and environment variable guidance are covered in GETTING_STARTED.md.
See GETTING_STARTED.md for:
- Supplying agent.yml (classpath vs file path)
- Mapping command names to handler bean names
- Output schema and exit expression guidance
- MCP server configuration within agent.yml
Purpose: Analyzes and remediates security vulnerabilities detected by Snyk.
Triggers: Snyk webhook events (new vulnerabilities)
Capabilities:
- Analyzes vulnerability details (CVSS score, exploit maturity)
- Provides remediation guidance (upgrade paths, patches)
- Suggests temporary mitigations (WAF rules, configuration changes)
- Creates validation plans and follow-up actions
Configuration:
# Triggered by Snyk webhooks to /api/webhooks/snyk
# Requires: SNYK_WEBHOOK_SECRET environment variablePurpose: Analyzes Jira issues and ensures they have sufficient information for development.
Triggers: Jira webhook events or manual trigger
Capabilities:
- Retrieves and analyzes Jira issue details
- Reviews comments and attachments
- Identifies missing information
- Adds clarifying questions as comments
- Updates issue status when ready for development
Configuration:
# Triggered by Jira webhooks to /api/webhooks/jira/{issueKey}
# Requires: ATLASSIAN_EMAIL, ATLASSIAN_SITE_URL, ATLASSIAN_API_TOKENPurpose: Automatically fixes bugs based on Jira tickets.
Triggers: Follows after Jira Issue Agent when issue is ready
Capabilities:
- Clones the repository from Jira issue
- Creates a feature branch
- Analyzes code to find root cause
- Implements and tests the fix
- Commits and pushes changes
- Documents the fix in Markdown
Configuration:
# Triggered after jira_agent completes
# Requires: GIT_SSH_PRIVATE_KEY for repository accessgraph LR
A[Jira Webhook] --> B[Jira Agent]
B --> C{Issue Ready?}
C -->|Yes| D[Bug Coding Agent]
C -->|No| E[Add Questions]
D --> F[Fix Branch Created]
F --> G[Handler Notifies Team]
Edit agent.yml to customize:
- Model: Change from
claude-4.5-sonnetto other models - Instructions: Modify the agent's behavior
- Output Schema: Change the structured output format
- MCP Servers: Add or remove tool access
- Exit Expression: Modify completion conditions
The application provides comprehensive health checks and Prometheus metrics for monitoring system health, MCP server status, and WebSocket connections.
See GETTING_STARTED.md for quick verification endpoints and example curl commands.
# Application information
curl http://localhost:8080/actuator/infoPrometheus-compatible metrics are exposed at /actuator/prometheus. See Docker stack provisioning in docker/monitoring/* for Prometheus and Grafana defaults. For local curl examples, see GETTING_STARTED.md.
The application tracks detailed metrics for Model Context Protocol (MCP) server operations:
qodo_mcp_active_servers- Number of active MCP server connectionsqodo_mcp_initialized_servers- Successfully initialized MCP serversqodo_mcp_failed_servers- MCP servers that failed to initializeqodo_mcp_servers_no_tools- MCP servers with no tools registeredqodo_mcp_registered_tools- Total number of registered tools across all servers
qodo_mcp_tool_invocations_total- Total tool invocations (tagged byserverandtool)qodo_mcp_tool_invocations_success- Successful tool invocations (tagged byserverandtool)qodo_mcp_tool_invocations_failure- Failed tool invocations (tagged byserverandtool)qodo_mcp_tool_execution_time- Tool execution duration timer (tagged byserverandtool)
Example Prometheus Queries:
# Average tool execution time by server
rate(qodo_mcp_tool_execution_time_sum[5m]) / rate(qodo_mcp_tool_execution_time_count[5m])
# Tool success rate
rate(qodo_mcp_tool_invocations_success[5m]) / rate(qodo_mcp_tool_invocations_total[5m])
# Failed servers alert
qodo_mcp_failed_servers > 0
The application tracks WebSocket connection health:
qodo_ws_active_connections- Number of active WebSocket connections in the JVM
Example Prometheus Queries:
# Alert when no WebSocket connections
qodo_ws_active_connections == 0
# WebSocket connection trend
rate(qodo_ws_active_connections[5m])
The custom McpServersHealthIndicator performs active health checks on MCP servers and reports status via the actuator health endpoint. See GETTING_STARTED.md for practical curl usage and examples.
-
Set up Prometheus scraping:
scrape_configs: - job_name: 'agent-sdk' metrics_path: '/actuator/prometheus' static_configs: - targets: ['localhost:8080']
-
Create alerts for critical metrics:
- MCP server failures:
qodo_mcp_failed_servers > 0 - No WebSocket connections:
qodo_ws_active_connections == 0 - High tool failure rate:
rate(qodo_mcp_tool_invocations_failure[5m]) > 0.1
- MCP server failures:
-
Monitor health endpoint:
- Use Kubernetes liveness/readiness probes
- Set up external monitoring (Pingdom, UptimeRobot)
- Configure load balancer health checks
-
Dashboard recommendations:
- MCP server status overview
- Tool execution performance by server/tool
- WebSocket connection stability
- JMS queue depth and processing rate
Use Case: You want to add a webhook endpoint for GitHub, GitLab, or any external service.
Create a new controller in src/main/java/ai/qodo/command/controllers/:
package ai.qodo.command.controllers;
import ai.qodo.command.internal.service.MessagePublisher;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import static ai.qodo.command.internal.service.MessagePublisher.MSG_TYPE;
/**
* Controller to handle GitHub webhook events.
* Supports events like push, pull_request, issues, etc.
*/
@RestController
@RequestMapping("/api/webhooks")
public class GitHubWebhookController {
private static final Logger logger = LoggerFactory.getLogger(GitHubWebhookController.class);
private static final String HEADER_EVENT = "X-GitHub-Event";
private static final String HEADER_SIGNATURE = "X-Hub-Signature-256";
private static final String MSG_GITHUB = "github_agent";
private final ObjectMapper objectMapper;
private final MessagePublisher messagePublisher;
private final GitHubWebhookValidator validator; // Create this similar to SnykWebhookValidator
@Value("${messaging.queue.event}")
private String eventTopic;
public GitHubWebhookController(ObjectMapper objectMapper,
MessagePublisher messagePublisher,
GitHubWebhookValidator validator) {
this.objectMapper = objectMapper;
this.messagePublisher = messagePublisher;
this.validator = validator;
}
@PostMapping("/github")
public ResponseEntity<?> handleGitHubWebhook(
@RequestHeader(value = HEADER_EVENT, required = false) String eventType,
@RequestHeader(value = HEADER_SIGNATURE, required = false) String signature,
@RequestBody String rawBody) {
logger.info("Received GitHub webhook - Event: {}", eventType);
try {
// Validate signature
if (!validator.validateSignature(rawBody, signature)) {
logger.error("GitHub webhook signature validation failed");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "Invalid signature"));
}
// Parse payload
Map<String, Object> payload = objectMapper.readValue(rawBody, Map.class);
// Add metadata
payload.put(HEADER_EVENT, eventType);
payload.put(MSG_TYPE, MSG_GITHUB);
payload.put("EventKey", String.format("%s_%s_%s",
eventType,
payload.get("repository"),
System.currentTimeMillis()));
// Publish to message queue
String message = objectMapper.writeValueAsString(payload);
messagePublisher.publish(eventTopic, message);
logger.info("Successfully processed GitHub webhook: {}", eventType);
return ResponseEntity.ok(Map.of("status", "success"));
} catch (Exception e) {
logger.error("Error processing GitHub webhook", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "Internal server error"));
}
}
@GetMapping("/github/health")
public ResponseEntity<?> healthCheck() {
return ResponseEntity.ok(Map.of(
"status", "healthy",
"service", "GitHub Webhook Handler",
"timestamp", Instant.now().toString()
));
}
}package ai.qodo.command.controllers;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@Component
public class GitHubWebhookValidator {
@Value("${github.webhook.secret}")
private String webhookSecret;
@Value("${github.webhook.validate-signature:true}")
private boolean validateSignature;
public boolean validateSignature(String payload, String signature) {
if (!validateSignature) {
return true;
}
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(
webhookSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKey);
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
String expected = "sha256=" + bytesToHex(hash);
return MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8)
);
} catch (Exception e) {
return false;
}
}
private String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}Add to src/main/resources/application.yml:
github:
webhook:
secret: ${GITHUB_WEBHOOK_SECRET:your-secret-here}
validate-signature: ${GITHUB_WEBHOOK_VALIDATION_ENABLED:true}Use Case: After an AI agent completes its workflow, you want to perform custom actions like sending notifications, updating databases, or triggering another agent.
CRITICAL CONCEPT: The handler bean name MUST exactly match the command name from agent.yml.
The mapping works as follows:
-
In
agent.yml, you define a command with a specific name:commands: github_agent: # ← This is the command name description: "..." instructions: "..."
-
In your Handler class, the
@Serviceannotation MUST use that exact command name + the suffix-handler:@Service("github_agent" + HANDLER_SUFFIX) // ← "github_agent" matches the command name public class GitHubAgentHandler implements Handler {
-
The constant
HANDLER_SUFFIXis defined in theHandlerinterface as"-handler", so:- Command name:
github_agent - Handler bean name:
github_agent-handler - Full annotation:
@Service("github_agent" + HANDLER_SUFFIX)
- Command name:
Real Examples from this Project:
| agent.yml Command Name | Handler Class | @Service Annotation | Bean Name |
|---|---|---|---|
snyk_agent |
SnykAgentHandler |
@Service("snyk_agent" + HANDLER_SUFFIX) |
snyk_agent-handler |
jira_agent |
JiraAgentHandler |
@Service("jira_agent" + HANDLER_SUFFIX) |
jira_agent-handler |
coding_agent |
CodingAgentHandler |
@Service("coding_agent" + HANDLER_SUFFIX) |
coding_agent-handler |
How the Framework Finds Your Handler:
When an agent completes its workflow, the framework:
- Reads the command name from the agent execution context (e.g.,
github_agent) - Appends
-handlerto create the bean name (e.g.,github_agent-handler) - Looks up the Spring bean with that exact name
- Calls the
handle()method on your handler
Common Mistakes to Avoid:
❌ Wrong: Using a different name in @Service
@Service("GitHubHandler") // Won't be found!❌ Wrong: Forgetting the HANDLER_SUFFIX constant
@Service("github_agent-handler") // Hardcoded, not recommended✅ Correct: Using the exact command name + HANDLER_SUFFIX
@Service("github_agent" + HANDLER_SUFFIX) // Perfect!Create a handler in src/main/java/ai/qodo/command/handlers/:
package ai.qodo.command.handlers;
import ai.qodo.command.internal.api.TaskResponse;
import ai.qodo.command.internal.pojo.CommandSession;
import ai.qodo.command.internal.service.MessagePublisher;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static ai.qodo.command.handlers.Handler.HANDLER_SUFFIX;
/**
* Handler for GitHub agent completion.
* Processes agent results and triggers follow-up actions.
*/
@Service("github_agent" + HANDLER_SUFFIX)
@Scope("prototype")
public class GitHubAgentHandler implements Handler {
private static final Logger logger = LoggerFactory.getLogger(GitHubAgentHandler.class);
private final MessagePublisher messagePublisher;
private final ObjectMapper objectMapper;
public GitHubAgentHandler(MessagePublisher messagePublisher, ObjectMapper objectMapper) {
this.messagePublisher = messagePublisher;
this.objectMapper = objectMapper;
}
@Override
public void handle(CommandSession commandSession, List<TaskResponse> allTaskResponses) {
String eventKey = commandSession.eventKey();
String sessionId = commandSession.sessionId();
logger.info("Processing GitHub agent completion - Session: {}, Event: {}",
sessionId, eventKey);
try {
// Extract structured output from agent responses
Map<String, Object> result = new HashMap<>();
result.put("sessionId", sessionId);
result.put("eventKey", eventKey);
result.put("timestamp", System.currentTimeMillis());
for (TaskResponse response : allTaskResponses) {
if ("structured_output".equalsIgnoreCase(response.type())) {
// Process structured output
result.putAll(response.data().toolArgs());
}
}
// Option 1: Publish to response queue for external consumption
messagePublisher.publishResponse(objectMapper.writeValueAsString(result));
// Option 2: Trigger another agent workflow
if (shouldTriggerNextAgent(result)) {
Map<String, Object> nextAgentPayload = buildNextAgentPayload(result);
messagePublisher.publishEvent(objectMapper.writeValueAsString(nextAgentPayload));
logger.info("Triggered next agent workflow for session: {}", sessionId);
}
// Option 3: Send external notification (webhook, email, etc.)
sendExternalNotification(result);
logger.info("Successfully handled GitHub agent completion: {}", sessionId);
} catch (Exception e) {
logger.error("Error handling GitHub agent completion", e);
}
}
private boolean shouldTriggerNextAgent(Map<String, Object> result) {
// Add your logic to determine if another agent should be triggered
return result.containsKey("requiresCodeReview") &&
Boolean.TRUE.equals(result.get("requiresCodeReview"));
}
private Map<String, Object> buildNextAgentPayload(Map<String, Object> result) {
Map<String, Object> payload = new HashMap<>();
payload.put("type", "code_review_agent");
payload.put("sourceSession", result.get("sessionId"));
payload.put("pullRequestUrl", result.get("pullRequestUrl"));
payload.put("EventKey", "code_review_" + System.currentTimeMillis());
return payload;
}
private void sendExternalNotification(Map<String, Object> result) {
// Implement external notification logic (Slack, email, etc.)
logger.info("Sending notification for result: {}", result);
}
}After creating your handler, verify the mapping is correct:
-
Check agent.yml - Find your command name:
commands: github_agent: # ← This is what you need
-
Check your Handler - Ensure @Service matches:
@Service("github_agent" + HANDLER_SUFFIX) // ← Must match exactly
-
Test the handler - Trigger the agent and check logs:
Successfully found handler: github_agent-handler
If you see "Handler not found" errors, the names don't match!
Use Case: You want to periodically poll an external API, check for updates, or trigger maintenance tasks.
Add to your main application class or create a configuration class:
package ai.qodo.command.internal.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
public class SchedulingConfig {
// Scheduling is now enabled
}package ai.qodo.command.internal.service;
import ai.qodo.command.internal.service.MessagePublisher;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
/**
* Scheduler to poll external API and trigger agent workflows.
*/
@Service
public class ApiPollingScheduler {
private static final Logger logger = LoggerFactory.getLogger(ApiPollingScheduler.class);
private final RestTemplate restTemplate;
private final MessagePublisher messagePublisher;
private final ObjectMapper objectMapper;
@Value("${polling.api.url}")
private String apiUrl;
@Value("${polling.api.enabled:false}")
private boolean pollingEnabled;
@Value("${messaging.queue.event}")
private String eventTopic;
public ApiPollingScheduler(RestTemplate restTemplate,
MessagePublisher messagePublisher,
ObjectMapper objectMapper) {
this.restTemplate = restTemplate;
this.messagePublisher = messagePublisher;
this.objectMapper = objectMapper;
}
/**
* Poll API every 5 minutes (300,000 ms).
* Use cron expression for more complex schedules: @Scheduled(cron = "0 */5 * * * *")
*/
@Scheduled(fixedDelay = 300000, initialDelay = 60000)
public void pollExternalApi() {
if (!pollingEnabled) {
return;
}
logger.info("Polling external API: {}", apiUrl);
try {
// Call external API
Map<String, Object> response = restTemplate.getForObject(apiUrl, Map.class);
if (response != null && hasNewData(response)) {
// Prepare message for agent processing
Map<String, Object> payload = new HashMap<>();
payload.put("type", "polling_agent");
payload.put("source", "scheduled_poll");
payload.put("data", response);
payload.put("EventKey", "poll_" + System.currentTimeMillis());
// Publish to event queue
String message = objectMapper.writeValueAsString(payload);
messagePublisher.publish(eventTopic, message);
logger.info("Published polling event to queue");
}
} catch (Exception e) {
logger.error("Error polling external API", e);
}
}
private boolean hasNewData(Map<String, Object> response) {
// Implement your logic to check if there's new data
return response.containsKey("updates") &&
!((List<?>) response.get("updates")).isEmpty();
}
/**
* Example: Daily cleanup task at 2 AM
*/
@Scheduled(cron = "0 0 2 * * *")
public void dailyCleanup() {
logger.info("Running daily cleanup task");
// Implement cleanup logic
}
}polling:
api:
url: ${POLLING_API_URL:https://api.example.com/updates}
enabled: ${POLLING_ENABLED:false}package ai.qodo.command.internal.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}Use Case: You want to add a new AI agent workflow or modify existing ones.
Add your agent configuration to src/main/resources/agent.yml:
commands:
github_agent:
description: "Analyze GitHub pull requests and provide code review feedback"
instructions: |
You are a code review assistant analyzing pull request: {/pullRequestUrl}
1. Fetch the PR details using the GitHub API
2. Review the code changes for:
- Code quality and best practices
- Security vulnerabilities
- Performance issues
- Test coverage
3. Provide actionable feedback
Output your analysis in the configured schema.
model: "claude-4-sonnet"
mcpServers: |
{
"mcpServers": {
"github-server": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "{GITHUB_TOKEN}"
}
}
}
}
tools: ["shell", "github-server"]
execution_strategy: "act"
output_schema: |
{
"properties": {
"success": {
"type": "boolean",
"description": "Whether the review was completed successfully"
},
"issues": {
"type": "array",
"items": {
"type": "object",
"properties": {
"severity": {"type": "string"},
"file": {"type": "string"},
"line": {"type": "number"},
"message": {"type": "string"}
}
}
},
"summary": {
"type": "string",
"description": "Overall review summary"
}
},
"required": ["success", "issues", "summary"]
}
exit_expression: "success"Follow the Creating a Custom Handler section to create GitHubAgentHandler.
Publish a message to the event queue with the correct type field:
Map<String, Object> payload = new HashMap<>();
payload.put("type", "github_agent");
payload.put("pullRequestUrl", "https://github.com/owner/repo/pull/123");
payload.put("EventKey", "pr_review_" + System.currentTimeMillis());
messagePublisher.publishEvent(objectMapper.writeValueAsString(payload));See GETTING_STARTED.md for application properties, environment variables, and MCP timeout guidance. The defaults for messaging, ports, and MCP are set in internal-core/src/main/resources/application-internal.yml and can be overridden in app/src/main/resources/application.yml or via environment variables.
The application uses a message queue (ActiveMQ) for event-driven architecture:
- Event Topic (
messaging.queue.event): Incoming events from webhooks/schedulers - Response Topic (
messaging.queue.response): Agent completion results - Audit Topic (
messaging.queue.audit): Audit logs
@Autowired
private MessagePublisher messagePublisher;
// Publish to event queue
messagePublisher.publishEvent(jsonMessage);
// Publish to response queue
messagePublisher.publishResponse(jsonMessage);{
"type": "agent_type",
"EventKey": "unique_event_id",
"timestamp": 1234567890,
"data": {
// Event-specific data
}
}- Unit and integration tests:
./gradlew test./gradlew integrationTest
- Webhook testing examples are consolidated in GETTING_STARTED.md.
- Controllers: Always validate signatures, log events, handle errors gracefully
- Handlers: Use
@Scope("prototype")for stateful handlers, follow naming convention - Schedulers: Make polling configurable, add circuit breakers for external APIs
- Messaging: Use transactions, handle redelivery, implement idempotency
- Configuration: Externalize secrets, use environment variables, provide defaults
- Logging: Use structured logging, include correlation IDs, log at appropriate levels
- Use environment variables for tokens and webhook secrets; never commit secrets to version control
- Validate webhook signatures using the raw request body (not parsed JSON)
- Avoid logging sensitive data (PII, tokens, passwords, API keys)
- Limit tools with
QODO_BLOCKED_TOOLSwhen necessary to restrict agent capabilities - Use HTTPS for all external webhook endpoints in production
- Rotate secrets regularly and use secret management tools (Vault, AWS Secrets Manager) in production
Operational troubleshooting and environment guidance are centralized in GETTING_STARTED.md. See that guide for handler mapping issues, agent.yml configuration, messaging, webhook validation, and MCP timeout adjustments.
- Java 21: Latest LTS version with virtual threads support
- Spring Boot 3.5.6: Enterprise-grade application framework
- Spring AI 1.0.1: AI/LLM integration framework
- Gradle 8.13: Build automation tool
- Apache ActiveMQ 6.1.4: Message broker for event-driven architecture
- Spring Integration: Enterprise integration patterns
- WebSocket (OkHttp3): Real-time bidirectional communication
- Model Context Protocol SDK 0.12.0: Tool integration for AI agents
- Spring AI MCP Client: MCP client implementation for Spring
- Claude/GPT-4 Support: Compatible with major LLM providers
- Spring Boot Actuator: Production-ready features
- Micrometer: Application metrics facade
- Prometheus: Metrics collection and alerting
- Custom Health Indicators: MCP server health checks
- Jackson: JSON/YAML/TOML processing
- Spring WebFlux: Reactive programming support for MCP
- JUnit 5: Unit testing framework
- Mockito: Mocking framework
- WireMock: HTTP API mocking
- Spring Boot Test: Integration testing support
- Docker: Container runtime
- Docker Compose: Multi-container orchestration
- Alpine Linux: Lightweight base image
- Virtual Threads: Java 21 lightweight concurrency
- Lombok (optional): Boilerplate code reduction
- Git: Version control
This project is licensed under the GNU General Public License v3.0 (GPL-3.0) - see the LICENSE.md file for details.
Copyright (C) 2025 Qodo
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
For issues, questions, or contributions:
- GitHub Issues: https://github.com/davidparry/agent-sdk/issues
- Documentation: This README and inline code documentation
- Examples: See the
appmodule for implementation examples
Please see our CONTRIBUTING.md for detailed guidelines on how to contribute, including code of conduct, development workflow, and guidelines for contributing to the internal-core module.
We welcome contributions such as:
- Bug fixes and feature enhancements to the core framework
- New example implementations in the
appmodule - Documentation improvements
- Testing enhancements
For quick reference when extending this application:
- Follow existing patterns (controllers, handlers, services)
- Add comprehensive logging
- Write unit and integration tests
- Update documentation (including this README and CONTRIBUTING.md)
- Use Spring Boot best practices
- Spring Boot team for the excellent framework
- Model Context Protocol team for the MCP specification
- Qodo team for the original architecture design
- Open source community for various libraries and tools