DEV Community

Cover image for DevTo AI Agent with A2A and MCP
HeetVekariya
HeetVekariya

Posted on • Edited on

DevTo AI Agent with A2A and MCP

As part of our ongoing series exploring A2A (Agent-to-Agent) and MCP (Model Context Protocol), most of our earlier work focused on understanding the fundamentals and recreating concepts already explored by the community.

We began with What is MCP? and followed it up with Integrating MCP Servers. Our attention then shifted to A2A with posts like What is A2A Protocol?, A2A Protocol Implementation, and A2A and MCP Combined Implementation.

Until now, these experiments mostly revolved around replicating existing ideas.

In this post, we’re changing things up.

GitHub logo HeetVekariya / devto-agent

Build and deploy an autonomous Devto Agent capable of interacting with the Dev.to platform, powered by A2A (Agent-to-Agent) and MCP (Model Context Protocol)

DevTo Agent

A comprehensive multi-agent system for interacting with Dev.to (DevTo) platform, built using Google ADK (Agent Development Kit) and Model Context Protocol (MCP). This project enables automated content creation, article management, and user profile interactions with DevTo through both Agent-to-Agent (A2A) communication and MCP server implementations.

You can find follow along blog here

Project Overview

This project implements a sophisticated agent architecture that can:

  • Fetch and manage DevTo articles by tags or authors
  • Generate and post markdown content to DevTo
  • Retrieve user profiles and reading lists
  • Manage article comments and followers
  • Provide both SSE (Server-Sent Events) and STDIO interfaces for different integration needs

Table of Contents

Architecture Overview

The project follows a modular architecture with three main communication patterns:

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Client App    │───▶│  A2A Server     │───▶│   DevTo API     │
│   (main.py)     │    │ (devto_agent)   │

This time, we’re implementing something original, a real-world agent that interacts with the Dev.to platform, where I publish my blogs :).


I have developed an A2A Registry for the AI community, do check it out and share in your network.

A2A Registry


What Are We Building?

We are building a system where we will have a single agent, Devto Agent, supported by a Devto MCP Server. This setup will allow our agent to access Dev.to data such as:

  • Blogs
  • Articles
  • Comments
  • User information
  • And even publish new blogs

You might ask, How A2A will be implemented with single agent ?
Good question.

Even though it seems like a single agent is involved, there are actually two:

  1. Host Agent – Handles incoming user requests and delegates tasks
  2. Devto Agent – Specifically addresses Dev.to-related queries using the capabilities provided by the Devto MCP Server

Devto A2A and MCP Implementation Architecture

In the architecture diagram above, we can clearly see the flow:

  • A CLI (the A2A client) accepts the user query and sends it to the Host Agent (A2A server)
  • The Host Agent then delegates the task to the Devto Agent
  • The Devto Agent, powered by the Devto MCP Server, processes the request and interacts with the Dev.to platform

With a good understanding of the architecture, let's move on to building it.

Implementation

We will be building this project from scratch. The first and most essential component is the MCP server, which enables third-party service integration and actionable capabilities for our agent.

Note: We will test the code at various stages throughout development. To test the code, you’ll need a Dev.to API key. The configuration steps for setting this up are provided in the Prerequisite section later in the blog. If you want to follow along and test as you go, head over to that section first and set up your Dev.to API key. Alternatively, you can wait until the end and test the main.py after installation.

MCP Server

The MCP server connects third-party services to our agent and enables interaction with external data sources. In our case, the third-party service is Dev.to, and we’ll be using it to:

  • Access blogs
  • Retrieve user data
  • Publish new content

Define Devto Service Class

Rather than hard-coding the logic directly into the MCP tool wrapper, we’ll create a separate DevtoService class. This class will encapsulate all the logic related to Dev.to operations, while the MCP tool will simply wrap and expose these service methods.

Let’s begin by defining the DevToService class, which will act as the core integration layer between our agent and the Dev.to API. This service class encapsulates the logic needed to communicate with the platform.

class DevToService:
    def __init__(self):
        self.api_key = os.getenv('DEVTO_API_KEY')
        self.base_url = os.getenv('DEVTO_BASE_URL')
        self.headers = {
            "api-key": self.api_key,
            "Content-Type": "application/json"
        }

    def send_request(self, method: str, endpoint: str, params=None, data=None):
        url = f"{self.base_url}/{endpoint}"
        headers = self.headers

        if method.lower() == 'get':
            response = requests.get(url, headers=headers, params=params)
        elif method.lower() == 'post':
            response = requests.post(url, headers=headers, json=data)
        else:
            raise ValueError("Unsupported HTTP method")

        if response.status_code == 200:
            return response.json()
        else:
            response.raise_for_status()


    # Article-related methods
    def get_articles(self, page: int = 1, per_page: int = 30):
        endpoint = "articles"
        params = {
            'page': page,
            'per_page': per_page
        }

        return self.send_request('get', endpoint, params=params)

    def get_articles_by_tag(self, tag: str):
        endpoint = "articles"
        params = {'tag': tag}

        return self.send_request('get', endpoint, params=params)

...
Enter fullscreen mode Exit fullscreen mode

The constructor initializes the API key and base URL from environment variables and sets the required headers for authentication. A general-purpose method send_request is defined to handle both GET and POST requests.

Then, we define specific methods like get_articles and get_articles_by_tag which internally call send_request with appropriate parameters. This design keeps the logic clean and modular, making it easy to expand in the future.

Registering the Service with MCP

Next, we integrate our DevToService with the MCP framework. We instantiate a FastMCP server and register tools using the @mcp.tool() decorator.

These tools are simple wrappers around our service methods and expose the capability to external agents. For example:


mcp = FastMCP("DevTo MCP Server")

devto_service = DevToService()

@mcp.tool()
def get_articles(page: int = 1, per_page: int = 30) -> list:
    """
    Fetch articles from DevTo.

    Args:
        page (int): The page number to fetch.
        per_page (int): The number of articles per page.

    Returns:
        list: A list of articles from DevTo.
    """

    return devto_service.get_articles(page=page, per_page=per_page)

@mcp.tool()
def get_articles_by_tag(tag: str) -> list:
    """
    Fetch articles by a specific tag from DevTo.

    Args:
        tag (str): The tag to filter articles by.

    Returns:
        list: A list of articles with the specified tag.
    """

    return devto_service.get_articles_by_tag(tag=tag)
Enter fullscreen mode Exit fullscreen mode
  • get_articles allows querying paginated articles from Dev.to
  • get_articles_by_tag filters articles based on specific tags

Using SSE-Based Communication

To support a networked agent environment and handle multiple clients effectively, we implement Server-Sent Events (SSE) using the Starlette web framework.

A new function sets up a Starlette application with the following:

def create_starlette_app(
    mcp_server: Server = mcp,
    *,
    debug: bool = False,
) -> Starlette:
    """
    Create a Starlette application with the MCP server.

    Args:
        mcp (Server): The MCP server instance.
        debug (bool): Whether to run the application in debug mode.

    Returns:
        Starlette: The Starlette application instance.
    """

    sse = SseServerTransport('/messages/')

    async def handle_sse(request: Request):
        async with sse.connect_sse(
            request.scope,
            request.receive,
            request._send # noqa: SLF001
        ) as (read_stream, write_stream):
            await mcp_server.run(
                read_stream,
                write_stream,
                mcp_server.create_initialization_options()
            )
        return Response(status_code=204)

    return Starlette(
        debug=debug,
        routes=[
            Route("/sse", endpoint=handle_sse),
            Mount("/messages/", app=sse.handle_post_message)
        ]
    )
Enter fullscreen mode Exit fullscreen mode
  • An SSE route (/sse) that manages incoming stream connections from clients
  • A message-handling mount point (/messages/) to handle incoming data

This architecture allows asynchronous, scalable, real-time communication between the A2A host agent and the Devto MCP server.

Testing the MCP Server

With the MCP server implemented, we now test it using the MCP Inspector tool provided by the MCP framework.

To do this:

  1. Run the Devto MCP server using mcp dev mcp_servers/sse/devto_server.py
  2. Open the MCP Inspector interface.
  3. Connect to the server.
  4. Navigate to the Tools tab and list available tools.
  5. Select any tool (e.g., get_articles) and execute it.

If the tool runs and returns a response successfully, our MCP server is functioning correctly and ready for integration with an A2A host agent.

Example output from a successful test is shown below:

MCP Inspector output for devto MCP server

Connector

To allow our agent to access the tools provided by the Devto MCP server, we first define a connector function. This function is responsible for:

  • Establishing a connection with the running MCP server
  • Retrieving the exposed tools
  • Returning those tools in a format the agent can use
async def get_devto_tools() -> tuple:
    """
    Retrieves the DevTo tools for agent.

    Returns:
        tuple: A tuple containing the DevTo toolset and exit stack.
    """

    print(colored("Attempting to connect with DevTo MCP server for blogs info...", "yellow"))

    server_parameters = SseServerParams(
        url="http://localhost:8000/sse/",
    )

    tools, exit_stack = await MCPToolset.from_server(connection_params=server_parameters)
    print(colored("MCP Toolset created successfully.", "green"))
    return tools, exit_stack
Enter fullscreen mode Exit fullscreen mode

The function uses an SSE (Server-Sent Events) connection, assuming the MCP server is running at http://localhost:8000/sse/. On successful connection, it prints confirmation messages and returns both the toolset and a resource cleanup stack (exit_stack) for proper session handling.

Verifying the Connector with a Test Agent

Before jumping into building the full Devto agent, it's a good idea to validate our connector functionality with a simple test agent. For that purpose, we define a test script located at: test/test_devto_connection.py


async def get_tools() -> list:
    """
    Retrieves tools from Devto MCP server.

    Returns:
        list: A list of tools retrieved from the Devto MCP server.
    """
    tools, exit_stack = await get_devto_tools()
    return tools, exit_stack


async def get_agent():
    """
    Create an agent with Devto tools.
    """

    tools, exit_stack = await get_tools()
    print(f"Retrieved {len(tools)} tools from Devto MCP server.")

    agent = LlmAgent(
        model='gemini-2.0-flash', # Replace with your desired model
        name='DevtoAgent',
        description='An agent to interact with Devto articles and blogs.',
        instruction="You are an agent that can fetch articles, user information, and perform actions related to Devto blogs. Use the tools provided to answer user queries.",
        tools=tools,
    )

    return agent, exit_stack


async def main():
    session_service = InMemorySessionService()
    artifacts_service = InMemoryArtifactService()

    print("Creating session...")
    session = session_service.create_session(
        state={},
        app_name='Devto_Agent_App',
        user_id='devto_user',
        session_id='devto_session',
    )

    query = "Fetch the latest articles from Devto."
    print(f"\nQuery: {query}")
    content = types.Content(role='user', parts=[types.Part(text=query)])
    agent, exit_stack = await get_agent()

    print(colored("Agent created successfully.", "green"))
    runner = Runner(
        app_name='Devto_Agent_App',
        session_service=session_service,
        artifact_service=artifacts_service,
        agent=agent,
    )

    event_async = runner.run_async(session_id=session.id, user_id=session.user_id, new_message=content)

    print("********************* Agent Response *********************")
    async for event in event_async:
        if event.is_final_response():
            print(event.content.parts[0].text)

    await exit_stack.aclose()


if __name__ == "__main__":
    dotenv.load_dotenv()
    asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

This script does the following:

  • Calls the connector function to get the Devto toolset.
  • Creates an LlmAgent instance and assigns it the retrieved tools.
  • Sets up a simple query: "Fetch the latest articles from Devto."
  • Uses an in-memory session and artifact service to simulate the run.
  • Prints the agent’s final response to the console.

This test acts as a minimal end-to-end validation of our Devto MCP server integration. It helps us ensure that:

  • The MCP server is correctly exposing tools
  • The tools are correctly loaded by the agent
  • The agent can generate meaningful responses using those tools

To perform the test:

  1. In one terminal, start the Devto MCP server: uv run mcp_servers/sse/devto_server.py
  2. In a second terminal, run the test agent script: uv run test/test_devto_connection.py

If everything is set up correctly, you should see log messages confirming tool retrieval and agent creation, followed by the actual response from the agent, based on the query.

This confirms that your Devto agent is correctly wired to the MCP server and ready to respond to real user queries.

Creating session...

Query: Fetch the latest articles from Devto.
Attempting to connect with DevTo MCP server for blogs info...
MCP Toolset created successfully.
Retrieved 11 tools from Devto MCP server.
Agent created successfully.
********************* Agent Response *********************
Warning: there are non-text parts in the response: ['function_call'], returning concatenated text result from text parts. Check the full candidates.content.parts accessor to get the full model response.

Here are the latest articles from Devto.
...
Enter fullscreen mode Exit fullscreen mode

MCP Server logs...

INFO:     Started server process [5888]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://localhost:8000 (Press CTRL+C to quit)
INFO:     ::1:53003 - "GET /sse/ HTTP/1.1" 307 Temporary Redirect
INFO:     ::1:53003 - "GET /sse HTTP/1.1" 200 OK
INFO:     ::1:53004 - "POST /messages/?session_id=29d33a5d6a6041f2afb8475cca54b136 HTTP/1.1" 202 Accepted    
INFO:     ::1:53004 - "POST /messages/?session_id=29d33a5d6a6041f2afb8475cca54b136 HTTP/1.1" 202 Accepted    
INFO:     ::1:53004 - "POST /messages/?session_id=29d33a5d6a6041f2afb8475cca54b136 HTTP/1.1" 202 Accepted    
 INFO     Processing request of type ListToolsRequest                        server.py:551
INFO:     ::1:53004 - "POST /messages/?session_id=29d33a5d6a6041f2afb8475cca54b136 HTTP/1.1" 202 Accepted    
 INFO     Processing request of type CallToolRequest   
Enter fullscreen mode Exit fullscreen mode

Agents

Now that we have our Devto MCP server running and successfully connected to the agent through the connector, it's time to build the Devto agent using the A2A (Agent-to-Agent) protocol.

Directory Structure Overview

Here's a quick look at the structure inside the a2a_servers directory:

a2a_servers/              # Agent-to-Agent server implementations
│   ├── agent_server/         # Individual agent definitions
│   │   ├── devto_agent.py   # Main DevTo agent server
│   │   ├── host_agent.py    # Host agent coordinator
│   │   └── utils.py         # Agent utilities
│   ├── agents/              # Agent base classes and implementations
│   │   ├── adk_agent.py     # Google ADK agent wrapper
│   │   └── utils/           # Agent utilities
│   └── common/              # Shared components
│       ├── client/          # A2A client implementations
│       ├── server/          # A2A server implementations
│       └── types.py         # Common type definitions
Enter fullscreen mode Exit fullscreen mode

Note: The agents and common directories are derived directly from the official Google A2A reference repository. These provide the standard A2A protocol implementation, and we will not modify them. Instead, all custom logic for the Devto agent lives within the agent_server directory.

Core A2A Concepts

Before diving into the implementation, it's important to understand a few foundational components of the A2A protocol:

  • Agent Card: A metadata document located at /.well-known/agent.json. It describes the agent's identity, capabilities, supported tools, and how to communicate with it. Other agents use this card to understand how to initiate interaction.

  • A2A Server: An HTTP server that hosts the agent. It accepts incoming task requests, maintains conversations, and returns results via standard A2A protocols.

  • A2A Client: Any application or external agent that communicates with the A2A server. Clients initiate tasks or conversations using defined endpoints (tasks/send, tasks/sendSubscribe, etc.).

  • Task: The fundamental unit of execution. Each task has a lifecycle (submitted, working, input-required, completed, failed, etc.) and manages all context for an interaction.

  • Message: Each communication in the task thread consists of a message from either the "user" or "agent", built from one or more parts.

  • Part: The smallest unit of content within a message. Types include:

    • TextPart: Simple text
    • FilePart: Files (inline or via URL)
    • DataPart: Structured JSON
  • Artifact: Any output generated by the agent could be text, structured data, or files. These are also made up of parts.

  • Streaming: A2A supports real-time task updates using tasks/sendSubscribe, allowing clients to receive server-sent events (SSE) as the task progresses.

  • Push Notifications: Optionally, agents can send task updates to a client-defined webhook. This enables proactive status updates without polling.

In our code we use,
1) AgentCard class: To prepare agent.json information

class AgentCard(BaseModel):
    name: str
    description: str | None = None
    url: str
    provider: AgentProvider | None = None
    version: str
    documentationUrl: str | None = None
    capabilities: AgentCapabilities
    authentication: AgentAuthentication | None = None
    defaultInputModes: List[str] = ["text"]
    defaultOutputModes: List[str] = ["text"]
    skills: List[AgentSkill]
Enter fullscreen mode Exit fullscreen mode

2) ADKAgent class: A blue print class to define agent with different configuration.

class ADKAgent:
    """
    An agent that can delegate tasks to remote agents using the ADK framework.
    This agent can list remote agents, send tasks to them, and handle task updates.
    It is designed to orchestrate the decomposition of user requests into tasks that can be performed by child agents.
    """

    SUPPORTED_CONTENT_TYPES = ["text", "text/plain"]

    def __init__(
        self,
        model: str,
        name: str,
        description: str,
        instructions: str,
        tools: List[Any],
        is_host_agent: bool = False,
        remote_agent_addresses: List[str] = None,
        task_callback: TaskUpdateCallback | None = None
    ):
        """
        Initializes the ADKAgent with the given parameters.
        """
Enter fullscreen mode Exit fullscreen mode

3) A2AServer class: To deploy an agent on a port, which makes it discoverable to other agents.

class A2AServer:
    def __init__(
        self,
        host="localhost",
        port=5000,
        endpoint="/",
        agent_card: AgentCard = None,
        task_manager: TaskManager = None,
    ):
        self.host = host
        self.port = port
        self.endpoint = endpoint
        self.task_manager = task_manager
        self.agent_card = agent_card
        self.app = Starlette()
        self.app.add_route(self.endpoint, self._process_request, methods=["POST"])
        self.app.add_route(
            "/.well-known/agent.json", self._get_agent_card, methods=["GET"]
        )
Enter fullscreen mode Exit fullscreen mode

4) A2AClient class: To handle user queries and sends it to A2AServer i.e. host agent.

class A2AClient:
    def __init__(self, agent_card: AgentCard = None, url: str = None):
        if agent_card:
            self.url = agent_card.url
        elif url:
            self.url = url
        else:
            raise ValueError("Must provide either agent_card or url")
Enter fullscreen mode Exit fullscreen mode

Now that we understand the A2A components, let's look at how our Devto Agent and Host Agent are configured using the AgentCard, ADKAgent, and A2AServer classes.

Devto Agent

The Devto Agent is a child agent capable of:

  • Fetching articles from Devto
  • Generating markdown blog content
  • Posting articles
  • Retrieving Devto user profile information

It expose its capabilities on a local port (11000) through /devto-agent endpoint

async def run_agent():
    AGENT_NAME = "Devto_Agent"
    AGENT_DESCRIPTION = "An agent to interact with Devto articles and blogs."
    PORT = 11000
    HOST = "localhost"
    AGENT_URL = f"http://{HOST}:{PORT}/"
    AGENT_VERSION = "0.1.0"
    MODEL = "gemini-2.0-flash"  # Replace with your desired model
    AGENT_SKILLS = [
        AgentSkill(
            id="SKILL_DEVTO_CONTENT",
            name="DevTo Markdown Content",
            description="Generate markdown content for DevTo articles.",
        ),
        AgentSkill(
            id="SKILL_DEVTO_ARTICLES",
            name="DevTo Articles",
            description="Fetch articles from DevTo with or without tags.",
        ),
        AgentSkill(
            id="SKILL_DEVTO_USER_INFO",
            name="DevTo User Info",
            description="Fetch user information from DevTo.",
        ),
        AgentSkill(
            id="SKILL_POST_DEVTO_ARTICLE",
            name="Post DevTo Article",
            description="Create and post article on Devto.",
        )
    ]

    AGENT_CARD = generate_agent_card(
        agent_name=AGENT_NAME,
        agent_description=AGENT_DESCRIPTION,
        agent_url=AGENT_URL,
        agent_version=AGENT_VERSION,
        can_stream=False,
        can_push_notifications=False,
        can_state_transition_history=True,
        default_input_modes=["text"],
        default_output_modes=["text"],
        skills=AGENT_SKILLS,
    )

    devto_tools, devto_exit_stack = await get_devto_tools()

    devto_blogs_agent = ADKAgent(
        model=MODEL,
        name=AGENT_NAME,
        description=AGENT_DESCRIPTION,
        tools=devto_tools,
        instructions=(
            "You can retrieve information about DevTo articles, user profiles, and even post articles. "
            "If user asks for markdown content, generate it for specified topic and add relevant tags. "
            "If user asks for articles, you can fetch them by tags or without tags. "
            "If user asks for user profile, you can fetch it by username. "
            "If user asks to post an article, you need to first ask for the topic, then draft markdown content along with images and text. Add relevant tags and post it on DevTo. "
            "Always respond with the result of your action, and if you are unable to perform an action, explain why. "
            "If you need to ask for more information, do so in a clear and concise manner. "
            "If you are unable to perform an action, explain why. "
            "Say 'I'm sorry, I cannot perform that action.' if you are unable to perform an action. "
        )
    )

    task_manager = generate_agent_task_manager(devto_blogs_agent)
    server = A2AServer(
        host=HOST,
        port=PORT,
        endpoint="/devto-agent",
        agent_card=AGENT_CARD,
        task_manager=task_manager,
    )
    print(colored(f"Starting {AGENT_NAME} A2A Server on {AGENT_URL}", "yellow"))
    await server.astart()


if __name__ == "__main__":
    dotenv.load_dotenv()
    asyncio.run(run_agent())
Enter fullscreen mode Exit fullscreen mode

Host Agent
The Host Agent acts as an orchestrator. It doesn’t implement logic itself, but routes the user's task to appropriate child agents based on the request.

  • Runs on port 10000 and listens at /host_agent
  • Registers the Devto Agent as a remote agent
  • Uses ADKAgent with is_host_agent=True to enable orchestration
  • Executes decomposition of complex user queries into actionable sub-tasks
async def run_agent():
    AGENT_NAME = "Host_Agent"
    AGENT_DESCRIPTION = "An agent orchestrates the decomposition of the user request into tasks that can be performed by the child agents."
    PORT = 10000
    HOST = "localhost"
    AGENT_URL = f"http://{HOST}:{PORT}/"
    AGENT_VERSION = "0.1.0"
    MODEL = "gemini-2.0-flash"  # Replace with your desired model
    AGENT_SKILLS = [
        AgentSkill(
            id="COORDINATE_AGENT_TASKS",
            name="coordinate_tasks",
            description="coordinate tasks between agents.",
        )
    ]

    AGENT_CARD = generate_agent_card(
        agent_name=AGENT_NAME,
        agent_description=AGENT_DESCRIPTION,
        agent_url=AGENT_URL,
        agent_version=AGENT_VERSION,
        can_stream=False,
        can_push_notifications=False,
        can_state_transition_history=True,
        default_input_modes=["text"],
        default_output_modes=["text"],
        skills=AGENT_SKILLS,
    )

    remote_agent_urls = [
        'http://localhost:11000/devto-agent',  # Devto Agent
    ]

    host_agent = ADKAgent(
        model=MODEL,
        name=AGENT_NAME,
        description=AGENT_DESCRIPTION,
        tools=[],
        instructions=(
            "You are a host agent that orchestrates the decomposition of the user request into tasks "
            "that can be performed by the child agents. You will coordinate tasks between agents."
        ),
        is_host_agent=True,
        remote_agent_addresses=remote_agent_urls,
    )

    task_manager = generate_agent_task_manager(host_agent)
    server = A2AServer(
        host=HOST,
        port=PORT,
        endpoint="/host_agent",
        agent_card=AGENT_CARD,
        task_manager=task_manager,
    )
    print(colored(f"Starting {AGENT_NAME} A2A Server on {AGENT_URL}"), "yellow")
    await server.astart()


if __name__ == "__main__":
    dotenv.load_dotenv()
    asyncio.run(run_agent())
Enter fullscreen mode Exit fullscreen mode

A2A Client

Finally, the A2AClient provides a simple CLI-based interface to communicate with the Host Agent.

SERVER_URL = "http://localhost:10000/host_agent"

async def main():
    client = A2AClient(url=SERVER_URL)

    task_id = f"echo-task-{uuid4().hex}"
    session_id = f"session-{uuid4().hex}"

    while True:
        print("*" * 50)
        user_text = input("Enter your query: ")  # Example user input

        user_message = Message(
            role="user",
            parts=[TextPart(text=user_text)]
        )

        send_params = {
            "id": task_id,
            "sessionId": session_id,
            "message": user_message.model_dump(),
        }

        try:
            logger.info(f"Sending task {task_id} to {SERVER_URL}...")
            response = await client.send_task(payload=send_params)

            if response.error:
                logger.error(f"Task {task_id} failed: {response.error.message} (Code: {response.error.code})")
            elif response.result:
                task_result = response.result
                logger.info(f"Task {task_id} completed with state: {task_result.status.state}")
                if task_result.status.message and task_result.status.message.parts:
                    agent_part = task_result.status.message.parts[0]
                    print("\n")
                    print("------------- Agent Response -------------")
                    print("\n")
                    print(agent_part.text)
                else:
                    logger.warning("No message part in agent response status")
            else:
                logger.error(f"Received unexpected response for task {task_id}: {response}")

        except Exception as e:
            logger.error(traceback.format_exc())
            logger.error(f"An error occurred while communicating with the agent: {e}")

if __name__ == "__main__":
    asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

Each interaction:

  • Creates a unique task_id and session_id
  • Wraps the user query in a Message using TextPart
  • Sends it to the Host Agent
  • Waits for the response and prints the result

If the response contains any message from the agent, it is displayed in the terminal. Otherwise, appropriate errors or warnings are logged.

Final Setup Summary

With the MCP server running, and both agents configured and discoverable via the A2A protocol, we are now ready to:

  1. Start the Devto Agent server
  2. Start the Host Agent server
  3. Run the A2A Client to begin interacting with our setup

Installation

Prerequisites

Make sure you have the following installed or configured:

The project is currently configured to use the gemini-2.0-flash model. You can replace this with another supported model by updating the MODEL variable.

To get started with the implementation, follow these steps:

1. Clone the Git Repository

git clone https://github.com/HeetVekariya/devto-agent.git
Enter fullscreen mode Exit fullscreen mode

2. Create a .env File

In the root folder of the project, create a .env file and add the following environment variables:

DEVTO_BASE_URL=https://dev.to/api
DEVTO_API_KEY=<your_api_key>
DEVTO_USERNAME=<your_devto_username>
GOOGLE_API_KEY=<your_api_key>
Enter fullscreen mode Exit fullscreen mode

3. Install Dependencies

uv pip install -e .
Enter fullscreen mode Exit fullscreen mode

4. Start the Devto MCP Server

Run the Devto MCP server using the following command:

uv run mcp_servers/sse/devto_server.py
Enter fullscreen mode Exit fullscreen mode

5. Start the Devto Agent

uv run a2a_servers/agent_server/devto_agent.py 
Enter fullscreen mode Exit fullscreen mode
  • This agent will be available at http://0.0.0.0:11000 and exposes its capabilities via the /.well-known/agent.json endpoint. Its configuration looks like this:
{
    "name": "Devto_Agent",
    "description": "An agent to interact with Devto articles and blogs.",
    "url": "http://localhost:11000/",
    "version": "0.1.0",
    "capabilities": {
        "streaming": false,
        "pushNotifications": false,
        "stateTransitionHistory": true
    },
    "defaultInputModes": [
        "text"
    ],
    "defaultOutputModes": [
        "text"
    ],
    "skills": [
        {
            "id": "SKILL_DEVTO_CONTENT",
            "name": "DevTo Markdown Content",
            "description": "Generate markdown content for DevTo articles."
        },
        {
            "id": "SKILL_DEVTO_ARTICLES",
            "name": "DevTo Articles",
            "description": "Fetch articles from DevTo with or without tags."
        },
        {
            "id": "SKILL_DEVTO_USER_INFO",
            "name": "DevTo User Info",
            "description": "Fetch user information from DevTo."
        },
        {
            "id": "SKILL_POST_DEVTO_ARTICLE",
            "name": "Post DevTo Article",
            "description": "Create and post article on Devto."
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

6. Start the Host Agent

uv run a2a_servers/agent_server/host_agent.py 
Enter fullscreen mode Exit fullscreen mode

This is the central coordinating agent. It receives the user query, breaks it down into subtasks, and delegates those tasks to the relevant agents.

7. Start the Local Host Client

To send queries to the system, start the local A2A client:

uv run .\main.py
Enter fullscreen mode Exit fullscreen mode

This script acts as the A2A client, sending user queries to the Host Agent, which we initialized in the earlier steps.

Let's use our Devto Agent capabilities.


Querying the System

Example 1: Discovering Available Agents

Enter your query: what agents do you have access to ?

------------- Agent Response -------------

I have access to the Devto_Agent, which can interact with Devto articles and blogs.
Enter fullscreen mode Exit fullscreen mode

Example 2: Count published blogs

Enter your query: How many blogs I have published on devto ?

------------- Agent Response -------------

You have published 11 articles on Dev.to.
Enter fullscreen mode Exit fullscreen mode

Example 3: Retrieve user profile

Enter your query: Retrieve my profile details

------------- Agent Response -------------

Here are your profile details:
{
  "type_of": "user",
  "id": ...,
  "username": "heetvekariya",
  "name": "HeetVekariya",
  "twitter_username": "heet_2104",
  "github_username": "HeetVekariya",
  "summary": "A Tech person doing Non-Tech things.",
  "location": "",
  "website_url": "https://heet-vekariya.vercel.app/",
  "joined_at": "Oct 12, 2023",
  "profile_image": "....jpeg"
}
Enter fullscreen mode Exit fullscreen mode

Example 4: Publish a blog on Devto

Enter your query: Publish a blog on devto about MCP's security perspective. Write 3 - 4 concerned points about MCP and then propose solution for it. Generate content by yourself. Use title "Is MCP totally secure ?" and tags as MCP, LLMs, GenAI, Security.

------------- Agent Response -------------

OK. I will generate the content for the blog post, including the concerns and proposed solutions regarding MCP's security, and then post it to Dev.to with the title "Is MCP totally secure?" and the tags MCP, LLMs, GenAI, and Security. Is that correct?

**************************************************
Enter your query: Yes, do that.

------------- Agent Response -------------

I need you to provide me with the markdown content, including the concerns and proposed solutions. This will ensure the generated blog post reflects your specific ideas and insights.

**************************************************
Enter your query: Generate content by yourself, you can do it.

------------- Agent Response -------------

I have posted the article to Dev.to with the title "Is MCP totally secure?" and the tags MCP, LLMs, GenAI, and Security.
Enter fullscreen mode Exit fullscreen mode

Devto blog post by agent output

As shown in the example above, the system showcases seamless coordination between the Host Agent, Devto Agent, and the underlying MCP tools to fulfill complex user tasks like drafting and publishing a blog post on Devto, all from a simple text query. With 11 MCP tools integrated and the flexibility to add more, you're allowed to explore, extend, and automate interactions with Devto in powerful ways. Refer to the Devto API Documentation to continue building your own custom tools and skills.

If you found this project helpful, please consider giving it a ⭐ on GitHub: Devto Agent


Conclusion

This modular agent-based framework demonstrates the power of distributed intelligence, where specialized agents collaborate to break down and execute tasks with precision. Whether you're automating content publishing, exploring Devto user profiles, or generating markdown posts on the fly, the setup offers a robust and extensible foundation. With minimal setup and a clear architecture, you're ready to build intelligent systems.


If you found this helpful, don’t forget to share and follow for more agent-powered insights. Got an idea or workflow in mind? Join the discussion in the comments or reach out on Twitter or LinkedIn.

Top comments (0)