Overview

Follow this tutorial to build a complete, working Express + React agent that uses the ATXP Client SDK to interact with MCP servers. If you’d just like to use the completed project without building it yourself, the complete code for this example can be found at atxp-dev/agent-demo.

Project setup

In this tutorial, we’re going to build an Express + React agent that will create an image based on a natural-language prompt using the Image MCP server and then uploads the image to the Filestore MCP server to make it available at a public URL.

Initialize the project

First, we need to initialize a new project. The easiest way to do this is to use the atxp-agent-starter template. This template is great for quickly getting started with a project that wraps the ATXP Client SDK in an Express server and React frontend. Out of the box, it provides a simple web interface for submitting prompts directly to an ATXP-powered MCP server and either passing the results to the web frontend or using them in another MCP server tool call. It also includes functions for sending progress updates to the frontend. Let’s create a new repository from the template and clone it to your local machine.
1

Create a new repository from the template

Visit this link to create a new repository from the template. You can name your repository whatever you want, but we’ll call it agent-demo in this tutorial. If you name your repository differently, make sure to replace agent-demo with the name of your repository in the following steps.

2

Clone the repository

Clone the repository to your local machine. You can do this by running the following command in your terminal:

git clone https://github.com/your-username/agent-demo.git
Make sure to replace your-username with your actual GitHub username, and agent-demo with the name of your repository if you named it differently.
If you created a private repository, you’ll need to make sure that you’re authenticated with GitHub in order to clone it. If you’ve set up SSH access with GitHub, you can clone the repository by running git clone git@github.com:your-username/agent-demo.git. If you haven’t set up SSH access with GitHub, you’ll need to supply your GitHub username and password when prompted to clone your repository.

3

Install dependencies

Install the dependencies for the frontend and backend by running the following command in your terminal:

cd agent-demo
npm run install-all

Set up your ATXP account

In order to have your agent pay for the MCP servers it uses, you need have an ATXP account. ATXP also supports paying for MCP servers using a Solana or Base wallet, but for this tutorial we’ll use an ATXP account.
1

Create an ATXP account

If you don’t have an ATXP account yet, create one and copy your ATXP connection string. It should look something like this:
https://accounts.atxp.ai?connection_token=<random_string>
2

Store your ATXP connection string in an environment variable

The best way to store your connection string is to create a .env file in the backend directory of your project and set the ATXP_CONNECTION_STRING environment variable to your connection string. The atxp-agent-starter template provides an example .env file in the backend directory calle env.example.As you might guess from the name, that file is meant to be an example and not the actual .env file that your project uses. Copy the env.example file to .env and then edit it with your connection string.
cp backend/env.example backend/.env
Then, edit the .env file and set the ATXP_CONNECTION_STRING environment variable to your connection string.
.env
ATXP_CONNECTION_STRING=https://accounts.atxp.ai?connection_token=<random_string>
Never commit your .env file to version control. It is a good idea to add your .env to your .gitignore file to prevent it from being committed.
echo .env >> .gitignore

Develop the agent

We’re ready to start coding our agent now. Because we used the atxp-agent-starter template to create our project, our codebase is already organized into a frontend and backend. The codebase is laid out as follows:
agent-demo/
├── backend/                # Express server
   ├── server.ts           # Main server file (TypeScript)
   ├── stage.ts            # Progress tracking utilities (TypeScript)
   ├── tsconfig.json       # TypeScript configuration
   ├── package.json        # Backend dependencies
   └── env.example         # Environment variables template
├── frontend/               # React application
   ├── public/             # Static files
   ├── src/                # React source code
   ├── App.tsx         # Main React component (TypeScript)
   ├── App.css         # Component styles
   ├── index.tsx       # React entry point (TypeScript)
   └── index.css       # Global styles
   ├── tsconfig.json       # TypeScript configuration
   └── package.json        # Frontend dependencies
├── package.json            # Root package.json with scripts
├── .gitignore              # Git ignore rules
└── README.md               # Top-level README

Set up the connections to the MCP servers

1

Define MCP server helper objects

In order to make using the ATXP MCP servers easier, let’s set up some helper objects in the backend/server.ts file for each of the MCP servers we want to use. The atxp-agent-starter template already contains an example helper object for the Image MCP server that is commented out. Let’s uncomment it.
backend/server.ts
// In-memory storage for texts (in production, use a database)
let texts: Text[] = [];

// TODO: Create a helper config object for each ATXP MCP Server you want to use
// See "Step 1" at https://docs.atxp.ai/client/guides/tutorial#set-up-the-connections-to-the-mcp-servers for more details.
// For example, if you want to use the ATXP Image MCP Server, you can use the following config object:
// Helper config object for the ATXP Image MCP Server
// const imageService = {
//   mcpServer: 'https://image.mcp.atxp.ai',
//   toolName: 'image_create_image',
//   description: 'ATXP Image MCP server',
//   getArguments: (prompt: string) => ({ prompt }),
//   getResult: (result: any) => {
//     // Parse the JSON string from the result
//     const jsonString = result.content[0].text;
//     return JSON.parse(jsonString);
//   }
// };

// Express API Routes
app.get('/api/texts', (req: Request, res: Response) => {
  res.json({ texts });
});
Now let’s add a helper object for the Filestore MCP server just below the Image MCP server helper object.
backend/server.ts
// Helper config object for the ATXP Image MCP Server
const imageService = {
  mcpServer: 'https://image.mcp.atxp.ai',
  toolName: 'image_create_image',
  description: 'ATXP Image MCP server',
  getArguments: (prompt: string) => ({ prompt }),
  getResult: (result: any) => {
    // Parse the JSON string from the result
    const jsonString = result.content[0].text;
    return JSON.parse(jsonString);
  }
};

// Helper config object for the ATXP Filestore MCP Server
const filestoreService = { 
  mcpServer: 'https://filestore.mcp.atxp.ai', 
  toolName: 'filestore_write', 
  description: 'ATXP Filestore MCP server', 
  getArguments: (sourceUrl: string) => ({ sourceUrl, makePublic: true }), 
  getResult: (result: any) => { 
    // Parse the JSON string from the result
    const jsonString = result.content[0].text; 
    return JSON.parse(jsonString); 
  } 
}; 

// Express API Routes
app.get('/api/texts', (req: Request, res: Response) => {
  res.json({ texts });
});
These helper objects will give us a consistent way to interact with the MCP servers, without needing to inline the MCP servers’ details each time we want to use them.
2

Create the ATXP client objects

For each MCP server we want to use, we need to create an ATXP client object. This object will be instantiated with our ATXP account details and the MCP server’s details. The atxp-agent-starter template already contains an example client object for the Image MCP server that is commented out. Let’s uncomment it.
backend/server.ts
  // Send stage update for client creation
  sendStageUpdate(requestId, 'creating-clients', 'Initializing ATXP clients...', 'in-progress');

  // TODO: Create a client using the `atxpClient` function for each ATXP MCP Server you want to use
  // See "Step 2" at https://docs.atxp.ai/client/guides/tutorial#set-up-the-connections-to-the-mcp-servers for more details.
  // For example, if you want to use the ATXP Image MCP Server, you can use the following code:
  // const imageClient = await atxpClient({
  //   mcpServer: imageService.mcpServer,
  //   account: account,
  // });

  // Send stage update for just before the MCP tool call
  sendStageUpdate(requestId, 'calling-mcp-tool', 'Calling ATXP MCP tool...', 'in-progress');
Now let’s add a client object for the Filestore MCP server just below the Image MCP server client object.
backend/server.ts
  const imageClient = await atxpClient({
    mcpServer: imageService.mcpServer,
    account: account,
  });

  // Create a client using the `atxpClient` function for the ATXP Filestore MCP Server
  const filestoreClient = await atxpClient({ 
    mcpServer: filestoreService.mcpServer, 
    account: account, 
  }); 

  // Send stage update for just before the MCP tool call
  sendStageUpdate(requestId, 'calling-mcp-tool', 'Calling ATXP MCP tool...', 'in-progress');

Call the MCP servers

Now that we have our MCP server helper objects and ATXP client objects, we can start calling the MCP servers. To do this, we’ll call the callTool function on the ATXP client object for each MCP server we want to use. We’ll wrap these calls in a try/catch block to gracefully handle any errors that may occur.
1

Call the Image MCP server

The atxp-agent-starter template already contains an example of calling the Image MCP server that is commented out. Let’s uncomment it by uncommenting the following lines:
backend/server.ts
try {
  // TODO: Call the MCP tool you want to use
  // For example, if you want to use the ATXP Image MCP Server, you can use the following code:
  // const result = await imageClient.callTool({
  //   name: imageService.toolName,
  //   arguments: imageService.getArguments(text),
  // });

  // Send stage update for MCP tool call completion
  sendStageUpdate(requestId, 'mcp-tool-call-completed', 'ATXP MCP tool call completed!', 'completed');

  // TODO: Process the result of the MCP tool call
  // For example, if you want to use the ATXP Image MCP Server, you can use the following code:
  // const imageResult = imageService.getResult(result);
  // console.log('Result:', imageResult);


  // Note: If you want to use the result of the MCP tool call in another MCP tool call, you will need
  // a nested try/catch block wrapping the call to the next MCP tool.

  // TODO: Save the result of the MCP tool call to the `newText` object
  // For example, if you want to use the result of the ATXP Image MCP Server, you can use the following code:
  //newText.imageUrl = imageResult.url;

  // Save the `newText` object to the `texts` array
  texts.push(newText);

  // Return the `newText` object to the frontend
  res.status(201).json(newText);
} catch (error) {
  console.error(`Error with MCP tool call:`, error);

  // Send stage update for MCP tool call error
  sendStageUpdate(requestId, 'mcp-tool-call-error', 'Failed to call ATXP MCP tool!', 'error');

  // Return an error response if MCP tool call fails
  const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
  res.status(500).json({ error: 'Failed to call ATXP MCP tool', details: errorMessage });
}
At this point, our agent is able to create an image based on a natural‑language prompt using the ATXP Image MCP server but isn’t doing anything with the response from the MCP server.Keen-eyed readers may have noticed that lines 21-23 in the above snippet looks like something that we might want to do with the results of calling the Image MCP server’s tool. However, we’re going to instead send the generated image to the Filestore MCP server and use the URL returned from the Filestore MCP server to display the image in our web frontend.
2

Call the Filestore MCP server

We want to upload the image generated by the Image MCP server to an S3 bucket using the ATXP Filestore MCP server so that we get a publicly accessible URL for the image. We only want to call the Filestore MCP server if the image generation was successful, so we’ll put the Filestore MCP server’s tool call inside of a try/catch block nested inside of the try block for our image generation tool call.First, let’s remove some code that we don’t need anymore.
backend/server.ts
// TODO: Save the result of the MCP tool call to the `newText` object
// For example, if you want to use the result of the ATXP Image MCP Server, you can use the following code:
//newText.imageUrl = imageResult.url;

// Save the `newText` object to the `texts` array
texts.push(newText); 

// Return the `newText` object to the frontend
res.status(201).json(newText); 
Then, add the following lines below the // Note: If you want to use ... comment, which calls the Filestore MCP server’s tool to store the image.
backend/server.ts
// Store the image in the ATXP Filestore MCP Server
try {
  const result = await filestoreClient.callTool({
    name: filestoreService.toolName,
    arguments: filestoreService.getArguments(imageResult.url),
  });
  console.log(`${filestoreService.description} result successful!`);
  const fileResult = filestoreService.getResult(result);
  newText.fileName = fileResult.filename;
  newText.imageUrl = fileResult.url;

  console.log('Result:', fileResult);

  // Send stage update for completion
  sendStageUpdate(requestId, 'completed', 'Image stored successfully! Process completed.', 'final');

  texts.push(newText);
  res.status(201).json(newText);
} catch (error) {
  console.error(`Error with ${filestoreService.description}:`, error);
  // Send stage update for filestore error
  sendSSEUpdate({
    id: requestId,
    type: 'stage-update',
    stage: 'filestore-error',
    message: 'Failed to store image, but continuing without filestore service...',
    timestamp: new Date().toISOString(),
    status: 'error'
  });
  // Don't exit the process, just log the error
  console.log('Continuing without filestore service...');

  // Still save the text with the image URL from the image service
  newText.imageUrl = imageResult.url;
  texts.push(newText);
  res.status(201).json(newText);
}
🐛 Bugs! 🐛Your IDE has likely highlighted TypeScript errors in the code we just added. The issue is that we’re trying to assign fileName and imageUrl properties to the newText object, but these properties aren’t defined in the Text interface. Let’s fix these type errors in the next step.
3

Modify the `Text` interface

We need to modify the definition of the Text interface to include the fileName and imageUrl properties. Make the following changes to the Text interface in the backend/server.ts file.
backend/server.ts
// Define the Text interface that will be used to pass the results of the MCP calls to the frontend
// TODO: Update this interface to have the properties you need for your use case
interface Text {
  id: number;
  text: string;
  timestamp: string;
  imageUrl: string; 
  fileName: string; 
}
We also need to modify the instantion of the newText object to include the fileName and imageUrl properties. Make the following changes to the newText object instantiation in the backend/server.ts file.
backend/server.ts
  // Create the object that will be updated with the results of the MCP calls and passed to the frontend
  let newText: Text = {
    id: Date.now(),
    text: text.trim(),
    timestamp: new Date().toISOString(),
    imageUrl: '', 
    fileName: '', 
  };
Our agent should now build with npm run build, but we’re not done yet. When our agent finishes generating an image and storing it with the Filestore MCP server, we want to actually see the image in the frontend .

Update the frontend

Recall that our web-based agent has two main components: the Express backend that we’ve been working on that uses the ATXP Client SDK to interact with the ATXP MCP servers, and the React frontend that we need to update in order to display the results of the MCP calls.
1

Update the `frontend/src/App.tsx` file

Our frontend React app also uses TypeScript, so we first need to update the frontend’s Text interface to include the imageUrl and fileName properties. Make the following changes to the Text interface in the frontend/src/App.tsx file.
frontend/src/App.tsx
// Define the Text interface to match the Text interface in the backend
// TODO: Update this interface to have the properties you need for your use case
interface Text {
  id: number;
  text: string;
  timestamp: string;
  imageUrl: string; 
  fileName: string; 
}
2

Display the image in the frontend

Now that we’ve updated the Text interface on both the frontend to match the structure we are passed from the backend, we can update the frontend to display the image.Add the following lines to the frontend/src/App.tsx file.
frontend/src/App.tsx
<div className="texts-list">
    {texts.map((text) => (
        <div key={text.id} className="text-item">
            <p className="text-content">{text.text}</p>
            {/* TODO: Implement displaying the properties you need for your use case */}
            {text.imageUrl && ( 
                <figure> 
                    <img src={text.imageUrl} alt={text.text} className="text-image" />
                    <figcaption>{text.fileName}</figcaption> 
                </figure> 
            )} 
            <small className="text-timestamp">
                Submitted: {formatDate(text.timestamp)}
            </small>
            {text.fileId && ( 
                <small className="text-fileId">
                    File ID: {text.fileId} 
                </small> 
            )} 
        </div>
    ))}
</div>
Our frontend React app is now ready to display the image that we’ve generated using the ATXP Image MCP server and made available at a public URL using the ATXP Filestore MCP server.

Testing the agent

We’re ready to test our agent! Run npm run dev from the root directory of your project to start the frontend and backend servers. If your browser doesn’t automatically open to http://localhost:3000, navigate there manually. Submit a simple prompt like Make an image of a car. You should see progress updates as your agent first generates the image and then store it at a publicly accessible URL, and finally see the image displayed!
Congratulations! You’ve successfully built and tested an agent that pays for MCP server tool calls using ATXP. You’ve seen how to use MCP tools sequentially, and you’ve generated a (hopefully) fun image.
You’re now an ATXP pro ready to build agents that can do whatever you can imagine.

Next steps