Introduction
This is a post I need to share with you my thought process, decisions that I took in designing the service, and share new things that I learned. I hope anyone can benefit from my words or inspire anyone with my ideas and what I learned so we both can grow.
GitHub Repository:
mazenaly256
/
LINTelligent
AI-integrated backend service for code linting (i.e. analyzing code for issues), with an accompanying MCP server for AI-agent integration.
LINTelligent
AI-integrated backend service for code linting (i.e. analyzing code for issues), with an accompanying MCP server for AI-agent integration.
Overview
A code snippet is submitted in a linting/reviewing request, either as a direct snippet in the request body or via a public GitHub file URL.
Once the linting request reaches the system, LINTelligent saves its details in the database and enqueues a background job to the job queue for handling this request, then immediately returns 202 Accepted. If a GitHub URL was provided, the code is first fetched, then the review job runs, otherwise the review job runs directly on the submitted code snippet.
After that, a worker picks up the job from the job queue and processes it, it fetches the code from the GitHub URL (if exists) then it calls the configured LLM provider with the request details and receive the review report of issues, then updates…
Fun Fact: While I was testing it and request reviews on some code snippets from my previous project (Cinema Seat Reservation System), LINTelligent helped me discover a critical bug related to resource wasting as I did not dispose some objects that results in memory leak.
Overview
LINTelligent is a service that takes a code snippet as an input, and analyze it and return the issues in this code snippet.
User can either use it directly by requesting the API endpoints, or via an AI agent through the MCP server.
The workflow
User sends a review request, the service stores a new review with the code snippet in the database with status 'Pending', then it stores in the queue the methods that should be called to process this job/request with the related arguments for this request, then the service immediately returns 202 Accepted HTTP response so the client do not wait too much, detaching the client from long-running background processing.
As the jobs are saved in the queue, a worker thread picks up a job of a review request from the queue and process it, calling the methods that are stored in database as the processing way for this job. The worker sends the review request with the system prompt and the code snippet (user message) to the LLM and receives the response, then the service persists the review report that LLM returned into the database, then it notifies the user that the the request processing has been finished.
System Architecture and Design
Lets start with a basic system architecture diagram with the data flow for the service.
Asynchronous Processing
I designed this system to handle the requests in an asynchronous way, so the requests are processed in the background and then the user gets notified on completion via webhook. To achieve this I used message/job queue pattern, as shown in the diagram, the service stores the review request as a job in the job queue, and then a worker execute that job by calling the LLM, getting the review report then notify the user via webhook.
Queue Pattern
The queue pattern is simply a pattern where a producer puts messages/jobs in a queue, and then a consumer/worker (that can be either on another machine or just another thread) that reads that job/message and executes some logic to process them.
In our case, the producer is obviously the service. Inside the endpoint of requesting a new review, it stores a job in a queue.
Now we need a queue that persists the jobs/messages so the workers can take and process them one by one. Also, we need the workers themselves.
Hangfire Decision
Hangfire is a technology that provides the queue and persist the jobs data in a database (stores the details about the method that is required to be called to process some logic).
Also Hangfire reserves a number of threads form the thread pool of the application to create the workers pool, these are threads that only execute the enqueued jobs, each worker polls for jobs from the database and when there is a job it directly execute it.
Because of that, I favored using Hangfire to implement this pattern. It is simpler and provide all-in-one solution for me and at my scale I believe that it is more than sufficient.
Other choices would be more complicated and will not pay off at my scale. I could implement this using a separate queue on another server via technologies like RabbitMQ or any similar message brokers but this will require having separate workers also that will listen to the messages or poll for them and implement the logic.
Handling User-Supplied URLs Safely (SSRF Mitigation)
When I added the ability to submit a GitHub file URL instead of pasting code directly, I ran into a security problem that's easy to miss: SSRF, or Server-Side Request Forgery.
The naive version of this feature is simple, take the URL the user gives you, and have your server fetch it. The problem is that "your server fetches it" part. The request doesn't come from the user's browser, it comes from your backend, using your backend's network position. If you don't restrict what URLs are allowed, an attacker isn't limited to real GitHub files, they can point your server at internal services, cloud metadata endpoints, or anything else your server can reach that the hackers normally couldn't.
So a URL field isn't just an input validation problem, it's a "what is my server willing to do on someone else's behalf" problem.
For handling this, I enforced exact host match on raw.githubusercontent.com, and prevented any redirections when requesting the submitted URL.
Integration with LLM and controlling the usage cost
When you call the LLM to integrate it in your system, you don't just send the code. You send two things every single call:
1. System Prompt
This is your instructions to the LLM, it never changes between calls. It tells the LLM who it is (the persona) and what to do. It is something like:
"You are a code review assistant. Analyze the code you receive and return ONLY a JSON object with an issues array. Each issue must have: line number, severity (error/warning/info), type, and message. Never explain yourself. Never add text outside the JSON."
This is how you force structured output, short and structured JSON and nothing else. Without it, the AI would respond in plain English like a human, and this is hard to process in the code also it makes your output tokens explode and thereby your cost.
That's exactly why the system prompt exists, to say "return JSON only, no explanations", it's not just about structure, it's also cost control. Furthermore, you handle edge cases through the system prompt, like telling the LLM that if the inferred language of the code snippet does not match the language field that is sent with the request, then return language mismatch, instead the LLM returns many compilation errors as the language is different, which is unnecessary cost.
2. User Message
This is the actual code the user submitted, and LLM deals with it according to the instructions in the system prompt. This changes every call according to the request data, this is the "What to handle". System prompt is the "How to handle".
Software Design (Architecture and Patterns)
I was building my application without caring about this at the beginning, but when I needed to perform Unit Testing, I realized that my code is tightly coupled and needs refactoring. So, in the next sections I will specify what did I design and build exactly.
Clean Architecture
When thinking in this project, it appears that it aligns well with the Clean Architecture. It has external dependencies like database, LLM, webhook notifying and GitHub integration, so they should be isolated in Infrastructure Layer. Inside this layer, I designed it to contain implementations for LLM clients that their job is only to communicate with LLM providers and it absolutely never do anything related to the business logic, and to contain implementations for repositories. Then I built an Application Layer that orchestrate the business workflow and decisions, so it talks to the infrastructure layer without being dependent on it, achieving loose coupling between layers.
This aligns with the Single Responsibility Principle. Also, it makes the codebase more testable as it isolates the client and make all the business workflow orchestration only inside the Application Layer. Now if I want to write unit tests, I will never care about the client as it is simply integration details, no need to consider them in unit testing.
Adapter design pattern
Appears in LLM client. It adapts the different responses of different LLMs, and give standard response to the Application Layer. So, the client itself adapts the internal shape of data to be compatible with the standard DTO defined inside the Application Layer.
Repository Design Pattern
Generally, having repository pattern with EF Core is actually a bad practice. The truth is EF Core itself applies repository pattern and unit of work pattern. So if you tried to implement repository pattern, it will be just wrapper methods over the EF Core, and it will hide the power of using EF Core directly and controlling the queries and commands via LINQ.
But in my case, I decided to have a repository pattern, actually I felt that it is important. In my case I just need some operations like: changing status, saving review result in the database or get review by ID. I did not need to delete anything or to generally update the records, I just needed to update specific fields and add new or get by ID, so in my case I found that it is better to have this layer that will introduce to me better API to use inside the application layer and fits better to my needs. Furthermore, the repository pattern here to achieve more loose coupling, and higher testability by using the interfaces and mocking, so it is a trade-off.
Integrating with the AI Agent
I wanted to explore what it means to make a service agent-ready, not just API-ready. I wanted to make the AI agent to become another consumer of the API, so not only frontend clients can use my service via APIs, also the AI agents via MCP Server.
What is MCP Server
AI Agent (more accurately, Host) contains two relevant pieces: the LLM (that is responsible for the reasoning) and the Client (that communicates with MCP servers).
The MCP server is essentially a layer for AI agents to use the API. REST API endpoints are great for developers building frontends that consume the API directly, but if you want AI agents to call your API endpoints, then you need this extra layer, the MCP server, communicating with AI agent either over stdio or HTTP, depending on whether it runs locally on the same machine as the agent, or the server is deployed online.
Docker Image
Docker image of the local MCP server is available in the packages section in the repository.
mazenaly256
/
LINTelligent
AI-integrated backend service for code linting (i.e. analyzing code for issues), with an accompanying MCP server for AI-agent integration.
LINTelligent
AI-integrated backend service for code linting (i.e. analyzing code for issues), with an accompanying MCP server for AI-agent integration.
Overview
A code snippet is submitted in a linting/reviewing request, either as a direct snippet in the request body or via a public GitHub file URL.
Once the linting request reaches the system, LINTelligent saves its details in the database and enqueues a background job to the job queue for handling this request, then immediately returns 202 Accepted. If a GitHub URL was provided, the code is first fetched, then the review job runs, otherwise the review job runs directly on the submitted code snippet.
After that, a worker picks up the job from the job queue and processes it, it fetches the code from the GitHub URL (if exists) then it calls the configured LLM provider with the request details and receive the review report of issues, then updates…
Communication between AI Agent and MCP Server
AI agent (especially the Client) does the MCP handshake via 'initialize' request to get more info about the MCP server and its capabilities then it sends 'tools/list' to know each server's tools, and the same with prompts with 'prompts/list' and resources with 'resources/list' so agent can can invoke those things when it needs. All happening at connection startup, before any actual request for any specific resource, prompt or tool. The actual request happens after that, when LLM decides to use any thing from these available tools during the reasoning loops.
Wrap-up
One of the things I learned in this project is what loose coupling really means in practice, I started with a tightly-coupled codebase in the beginning then I had to refactor the whole codebase and architecture to make it testable and write good unit tests.
I learned many things from this project and shared with you some of them, I hope anyone can learn anything from this blog post or even take a glimpse on something new that can explore more deeply.

Top comments (0)