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/atxp-express-example.
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.
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.
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.
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 Base wallet, but for this tutorial we’ll use an ATXP account.
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>
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.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.
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
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.// 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.// 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. 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. // 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. 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.
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: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. 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.// 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.// 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.
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.// 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. // 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.
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.// 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;
}
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.<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