Sitemap

MCP in simple words

10 min readAug 21, 2025

--

MCP is becoming increasingly popular in our industry, yet many engineers haven’t had the opportunity to gain hands-on experience and become familiar with it. Here is a short article to catch up.

Press enter or click to view image in full size

The key takeaway from this article is that building MCP servers is a relatively easy thing to do! This is just normal engineering work that you always did. There are nuances, and of course, it has a different way of optimization and many aspects that we won’t cover here. Yet, I got the impression after talking with engineers that they think they will have to train something, that LLM will invoke their resources directly, or that some dark magic is involved. It is not, and you can totally master it!

Simple MCP Interaction Flow

Press enter or click to view image in full size

From the perspective of your server interactions, it appears as follows.

  • Client loads the list of supported tools on the MCP servers to which it has access
  • User puts a prompt into their client (let’s say VSCode or Claude)
  • Client sends your prompt along with a list of tools it knows to the LLM (OpenAI, Anthropic, AWS, local Ollama)
  • LLM suggests which tools (if any) to use to best handle your prompt
  • You are approving/declining the tool invocation
  • When you approve, logic is processed internally on your machine, and output is returned to the LLM to be processed and returned to your client.

This is part when many engineers gets confued as they think that LLM is getting access or is calling some endpoints directly. LLMs are just recommending invocation of tool(s). This is why when you are calling some tool with access to the private APIs or communicating with some DB, then whole processing is inside of the tool logs on your server and LLM will only see output of that action, not the call itself

  • Then the process is repeated, completed, or you may be asked follow-up questions to clarify.

Of course, in a happy path scenario. Sometimes, LLMs can invoke plenty of tools. Sometimes wrong ones — which means that your tool descriptions might be vague, or sometimes they might be even invoked on a loop with parameters that might seem… sub-optimal, not to say weird or stupid. Yet this is rare behaviour.

Quite often, LLMs recommend gathering a lot of information about the same context using different tools in case they can detect that they can get data using the same business key. For example, if you have a few tools for loading data based on orderId, productId, customerId, and so on, LLM will advise to invoke plenty of them to get the full picture.

Since we are sending a list of tools along with their description to the LLM, it means that all of that uses contexts and our tokens, which effectively impacts the cost of the solution. The same story is about suboptimal tools invocation.

Protocols

What are the protocols of communication between MCP clients and servers?

stdio

Simple communication based on standard I/O between processes (yes, it means that if you put something on an output, it will affect the process, so you have to log using stderr). Thanks to that, you have minimal setup, low latency, and no network dependencies.

As you can imagine, it is 1–to-1 process, so it is not scalable as it’s a local machine only. Yes, it is exotic.

SSE

Got deprecated, let’s move on

Streamable-HTTP

Regular communication over HTTP on a web server so that we can scale and support many clients. More complex, much more powerful. As long as you are using one of the MCP frameworks, you can focus on auth and implementing server capabilities. If you are interested in all the specification details, you can find them here.

Basic concepts

There are a couple of basic concepts in MCPs that you have to be aware of. Code samples are in the mcp-framework that is used here as an example due to its simplicity (although I wouldn’t use it for production workloads).

Resources

You can add extra context to your conversation using resources that can be exposed from the server (that way, they can be shared with all users of an MCP Server). You can add various types of resources, but they will likely be in JSON, XML, CSV, or PDF formats. Other types of media are also supported (like audio or images). That’s it.

There are of course plenty of implementations, but I came across very sneaky limitation that was not taking into account resource I provided due to its size. Bug is still open for that one: https://github.com/microsoft/vscode-copilot-release/issues/4074

Code sample

You can load that resource dynamically or return a file from the server.

import { MCPResource, ResourceContent } from "mcp-framework";
import {fetchWrapped} from "../http/wrapper.js";
import {Airport} from "../tools/locate/model.js";
import {Request} from 'node-fetch';

class AirportsResource extends MCPResource {
uri = "resource://countries.json";
name = "Countries";
description = "List of active Ryanair's countries.";
mimeType = "application/json";

async read(): Promise<ResourceContent[]> {
const url = "https://services-api.ryanair.com/views/locate/3/countries/en";
const airports: Airport[] = await fetchWrapped(new Request(url))
return [
{
uri: this.uri,
mimeType: this.mimeType,
text: JSON.stringify(airports),
},
];
}
}

export default AirportsResource;

How to use

Even according to the specs, resource usage may vary based on the client, as shown in the screen below.

Press enter or click to view image in full size
source: https://modelcontextprotocol.io/specification/2025-06-18/server/resources#user-interaction-model

In the case of VSCode, you can attach a resource as follows:

Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size
Attaching resource flow

Example flow could look like the following screen:

Press enter or click to view image in full size
Extend client context with resource

Tools

That’s the most crucial part here. Think of a tool as a function that is invoked on a server based on an LLM recommendation and with recommended parameters. In the simplest scenario, it is a function with SOME input that returns a String. However, it supports other types of media, such as audio, images, or resource links.

Since it’s a function, what does it imply? That you can do anything inside of it. You can bootstrap a project from a cookie-cutter, trigger a lambda function on AWS, run an Athena query, load data from DynamoDB, or even summarize your last week's tasks based on the JIRA API.

You can also drop a database or delete files. This is why it is essential to prioritize security and input sanitization. The same rules apply here as in regular software engineering.

Since we can perform various operations, tools also come with some hints about their effects, as shown in the table below.

Press enter or click to view image in full size
Tool annotations

Regarding implementation, there is no magic. Take a look at the code sample below. Model on input, logic to process it, and returns an output.

Code sample

import { MCPTool } from "mcp-framework";
import { z } from "zod";
import {fetchWrapped} from "../../http/wrapper.js";
import {Request} from 'node-fetch';

interface RouteDaysScheduleInput {
departure: string;
arrival: string;
}

class RouteDaysScheduleTool extends MCPTool<RouteDaysScheduleInput> {
name = "schedule-get-route-days-schedule";
description = "Get Ryanair's flight schedule dates based on the IATA code of departure airport and arrival airport IATA code";

schema = {
departure: {
type: z.string().regex(/^[A-Z]{3}$/).toUpperCase(),
description: "Departure IATA code (e.g. WAW)",
},
arrival: {
type: z.string().regex(/^[A-Z]{3}$/).toUpperCase(),
description: "Arrival IATA code (e.g. WAW)",
},
};

async execute(input: RouteDaysScheduleInput) {
const dep = input.departure.toUpperCase();
const arr = input.arrival.toUpperCase();

const results: string[] = await fetchWrapped(new Request(`https://services-api.ryanair.com/farfnd/v1/schedules/${dep}/${arr}/availability`))
if (results.length === 0) {
return `No flights found from ${dep} to ${arr}.`;
}

return results;
}
}

export default RouteDaysScheduleTool;

Usage

You can see the MCP interaction below. We are just using tools by interacting with the chat after setting up the MCP Server. In the section before changes, you can see LLM respond that it doesn’t know how to help you. In the section after adding the tool, you can see the flow with invoked tools by the LLM to achieve its goal. We can then ask follow-up or clarifying questions.

Press enter or click to view image in full size
Press enter or click to view image in full size

Prompts

Long story short, it is a prompt template that you can enrich with predefined parameters. It will just populate a text in your chat window.

Press enter or click to view image in full size

Code sample

import { MCPPrompt } from "mcp-framework";
import { z } from "zod";

interface PromptInput {
departureIataCode: string;
}

class RoundTripPrompt extends MCPPrompt<PromptInput> {
name = "round-trip-suggest";
description = "Suggests a round trip based on the departure IATA code and the user's preferences for direction and duration.";

schema = {
departureIataCode: {
type: z.string().regex(/^[A-Z]{3}$/).toUpperCase(),
description: "Departure IATA code (e.g. DUB)",
required: true,
}
};

async generateMessages(input: PromptInput) {
return [
{
role: "user",
content: {
type: "text",
text: `Load all flight directions departing from ${input.departureIataCode}`
},
},
{
role: "assistant",
content: {
type: "text",
text: `Which direction are you interested in?`
},
},
{
role: "assistant",
content: {
type: "text",
text: `For how many days you would like to go?`
},
},
{
role: "user",
content: {
type: "text",
text: `Suggest me a trip dates on this route for the specified period in during next 3 months`
},
},
];
}
}

export default RoundTripPrompt;

Usage

In the case of VSCode, you can put / and it will suggest using a prompt, then you will be guided to collect all of the parameters, and it will “render” that prompt into the chat window.

Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size

Debugging

Before using tools with LLM, we should always debug our configuration to ensure that our tool is working correctly based on deterministic input. We should first ensure that it works as expected, and only then expose the tools to the LLMs.

We can do so by running MCP Inspector with a command:

npx @modelcontextprotocol/inspector

However, it depends on how you are running your app. In case of running an app using stdio, you could go with a command like

npx @modelcontextprotocol/inspector node dist/index.js

# OR EVEN

npx @modelcontextprotocol/inspector -e MY_ENV_VAR="$MY_ENV_VAR" node dist/index.js

In case you are using some environment variables you can pass them in order to not set them every time you will run an inspector

Inspector

After running the command above, we will see the MCP Inspector, which will allow us to easily interact with our MCP Server. It supports a wide range of MCP features, including some more complex ones, such as authentication flows (more about this in the next post).

Based on the tools example, we can invoke tools, see their responses, and view logs in the case of invoking the server in stdio mode. In case of running over HTTP, logs can be found on the server (and thanks to that, we can use normal logging levels).

Press enter or click to view image in full size
Press enter or click to view image in full size
Inspector usage examples

How to use your MCP Server?

After we have built our server and are sure that everything works correctly, we can connect our server to an MCP Client, such as VSCode, using a configuration like the one below:

{
"servers": {
"first-mcp": {
"type": "stdio",
"command": "node",
"args": [
"<YOUR_PATH_TO_PROJECTS_DIR>/mcp-workshop/first-mcp/dist/index.js"
]
},
"first-mcp-http": {
"type": "http",
"url": "http://localhost:3000/mcp"
}
},
"inputs": []
}

When you will set it locally you should see information about status and server capabilities

You can also see the list of MCP tools that you have access to

Press enter or click to view image in full size

and also to enable or disable some of them by ticking the right boxes

Press enter or click to view image in full size

Summary

If you are new to this topic, I hope I have helped you understand the basics of MCP. Yet, you have to do some hands-on work and play with it yourself to grasp a concept. This opinion is based on running workshops with many engineers, not only on gut feeling. You can check yourself some very basic examples in this repo, where you can go commit by commit and check how to define super basic resources. It is available under https://github.com/SodaDev/mcp-workshop/commits/main/

Press enter or click to view image in full size

Remember that those samples are dummy ones. I recommend creating something close to your domain, a pet project, or something you are comfortable with, so you can quickly and easily validate the results. I always recommend starting with something that consumes your time during a work week.

Still lost?

You may have more questions about the flow or MCP in general. If that’s true, I recommend watching Confluent’s video on MCP. It is a true gem! In case you are still lost, please write to me using the links below, and please let me know how you like MCP and about your progress!

--

--