
Giving an LLM superpowers using MCP
No LLM can exist in vacuum today. In earlier times major vendors used to compete with each other by rolling out models that had bigger size, but after a while it became evident, that at a certain point the size matters little. The model can't know everything and be constantly up-to-date, and even if it could, the cost of running such a model would be prohibitive.
If the model lacks data, it will either try web search (if available), or simply start hallucinating.
The problem became obvious soon enough, so the companies decided to go towards a different direction: if a model doesn't know something, it will know how to find out. The first attempt was to give the models RAG (Retrieval-Augmented Generation) capabilities, but that was somewhat cumbersome and limited.
Another way of solving the problem was to let the model use external resources at its own discretion. This is how MCP was born. MCP stands for "Model Context Protocol" - a framework developed by Anthropic. It allows to extend the capabilities of an LLM by giving it access to external tools and resources.
You will need to make use of MCP if you want to achieve the following:
- Connect the model to a knowledge base / database that isn't exposed via Internet (so the model can't simply "google" it) (e.g., internal company knowledge base)
- Give the model control over an external device/environment/software (e.g. a CRM, a database, a file system, your local machine / browser / home IoT device / fridge / dishwasher, etc.) You can literary say "Jarvis, assemble this suit and paint it!"
- Allow the model to connect to a public API (e.g. weather, news, etc.), or a private API protected by an API key.
Don you know any other case? Let me know!
There are two ways to run it:
- On your local machine as a CLI command. In this case, the MCP client will spawn a process, send the input to STDIN and read the output from STDOUT. Simple as it is.
- As a cloud-native application deployed somewhere (in the cloud or on localhost). The MCP client then connects to the server over the network and consumes the output via an HTTP stream or SSE.
Your client should support MCP. For example Claude Desktop and Cursor have a section in their settings, containing a list of MCP servers installed.
Sometimes a client refuses to run a certain server. In this case you have two options:
- Explicitly tell the model to use it by adding a special instruction to the prompt: "give me information X, use mcp server Y"
- Save a rule in the model's settings to always use a certain server.
I've created a simple MCP server that retrieves the current weather in a given city using WeatherAPI. The service is reliable enough even on free plan (95% uptime), has generous free tier, and I used it for one of my IoT projects earlier.
This is a CLI tool written in TypeScript using @modelcontextprotocol/sdk.
It's pretty simple and exposes only one tool:
#!/usr/bin/env nodeimport { Server } from '@modelcontextprotocol/sdk/server/index.js';import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';import {CallToolRequestSchema,ListToolsRequestSchema,} from '@modelcontextprotocol/sdk/types.js';import { z } from 'zod';import { getWeatherData } from './tools/getWeather.js';const server = new Server({name: 'Weather API MCP Server',version: '0.1.0',},{capabilities: {tools: {},},});server.setRequestHandler(ListToolsRequestSchema, async () => {return {tools: [{name: 'get_weather',description: 'Get the weather for a given location',inputSchema: {type: 'object',properties: {location: {type: 'string',description: 'The location to get the weather for',},},required: ['location'],},},],};});server.setRequestHandler(CallToolRequestSchema, async (request) => {switch (request.params.name) {case 'get_weather': {const locationSchema = z.object({location: z.string().min(1, 'Location is required'),});const parseResult = locationSchema.safeParse(request.params.arguments);if (!parseResult.success) {throw new Error(parseResult.error.errors[0].message);}const { location } = parseResult.data;return {content: [{type: 'text',text: await getWeatherData(location),},],};}default:throw new Error('Unknown tool');}});async function main() {const transport = new StdioServerTransport();await server.connect(transport);}main().catch((error) => {console.error('Server error:', error);process.exit(1);});
What is important here:
- The shebang at the top indicates that the file is an executable.
- The Server is created with a name and a version.
- The
server.setRequestHandler()
method allows adding functionality to the server. There are three main cathegories of features the server may provide:- List of resources (e.g. files, notes, etc.) using
ListResourcesRequestSchema/ReadResourceRequestSchema
- List of tools using
ListToolsRequestSchema/CallToolRequestSchema
- List of prompts using
ListPromptsRequestSchema/GetPromptRequestSchema
- so basically the server can suggest a better prompt to the model.
- List of resources (e.g. files, notes, etc.) using
- The zod package is used to validate the input and output of the tools. It offers a nice declarative way opposite to the bunch of if-else statements.
Each tool is defined in it's own separate file:
import { z } from 'zod';import { loadConfig } from '../lib/config.js';import { BASE_URL, customFetch } from '../lib/customFetch.js';const getWeatherDataResultSchema = z.object({location: z.object({name: z.string(),region: z.string(),country: z.string(),lat: z.number(),lon: z.number(),tz_id: z.string(),localtime_epoch: z.number(),localtime: z.string(),}),current: z.object({last_updated_epoch: z.number(),last_updated: z.string(),temp_c: z.number(),temp_f: z.number(),is_day: z.number(),condition: z.object({text: z.string(),icon: z.string(),code: z.number(),}),wind_mph: z.number(),wind_kph: z.number(),wind_degree: z.number(),wind_dir: z.string(),pressure_mb: z.number(),pressure_in: z.number(),precip_mm: z.number(),precip_in: z.number(),humidity: z.number(),cloud: z.number(),feelslike_c: z.number(),feelslike_f: z.number(),windchill_c: z.number(),windchill_f: z.number(),heatindex_c: z.number(),heatindex_f: z.number(),dewpoint_c: z.number(),dewpoint_f: z.number(),vis_km: z.number(),vis_miles: z.number(),uv: z.number(),gust_mph: z.number(),gust_kph: z.number(),}),});export async function getWeatherData(requestedLocation: string): Promise<string> {const config = loadConfig();const API_KEY = config.weather_api_key;const url = `${BASE_URL}/current.json?key=${API_KEY}&q=${encodeURIComponent(requestedLocation)}&aqi=no`;const data = await customFetch(url);const parsed = getWeatherDataResultSchema.parse(data);const location = parsed.location;const current = parsed.current;return `Weather for ${location.name}, ${location.country} is ${Math.round(current.temp_c)} degrees Celsius, feels like ${Math.round(current.feelslike_c)} degrees Celsius, humidity is ${current.humidity}%, the wind speed is ${current.wind_kph} km/h, the pressure is ${current.pressure_mb} hPa, cloud cover is ${current.cloud}%, visibility is ${current.vis_km} km, condition: ${current.condition.text}`;}
What's important here:
- The output of the tool is plain text, because models work with textual data.
- A custom fetch function is used, this function basically adds retry logic and a timeout, plus handles errors.
- The secret is provided by the loadConfig() function. Don't store secrets as environment variables in
~/.bashrc
or~/.zshrc
: the server won't see them. Even though the server is a CLI tool, it's parent process isn't shell: it's either ClaudeCode or Cursor that spawn from the task bar that doesn't read shell startup files. Instead, put the secret into a config file, for example~/.weather-api-mcp.yml
.
I've installed the server to Claude Desktop by adding the following entry to claude_desktop_config.json
:
{"mcpServers": {..."weather-api-mcp": {"command": "/Users/username/weather-api-mcp/build/index.js"}}}
Now I can ask the model to give weather-aware suggestions:

On the screenshot it's clearly visible that the model uses the weather MCP without even being told to do so. It's also evident that the model doesn't google anything for this kind of prompts anymore.
Quite impressive!
The source code is available on GitHub.
Every new technology comes with it's own security risks. MCP is no exception.
The most obvious security consideration is: you don't own the model you are using. Everything the model gets from MCP is eventually sent to the vendor platform (OpenAI, Anthropic, etc.). If you have sensitive data in your corporate database, you need to be extra careful when using MCP.
Today you can enable the privacy mode on some vendors, but nonetheless, as soon as the data is out, it's a leap of faith. Some vendors may log prompts for debugging/analytical purposes, so you need to be careful with what you send to them.
How to mitigate:
- Make sure your MCP has restrictions in place to prevent sensitive data from leaking out (e.g., corporate secrets, passwords, access codes)
- Consider running your model on premises, within the security perimeter, or in a private cloud.
Often resources require API keys or access tokens to be used. If you are using a public MCP server, you need to be extra careful with credentials. It's not a good idea to store the credentials in the server settings on the IDE side, because they may be transferred to the model over the wire as part of the prompt. The same goes for sending the credentials in a prompt explicitly.
How to mitigate:
- Store and use the credentials on the MCP server's side, it prevents the model from seeing them.
The nature of this and the next vulnerability comes from the fact that with LLMs there isn't any real separation between data and instructions (unlike in the traditional imperative programming). That means, everything the model is able to reach out to eventually becomes a part of the prompt the model uses to decide what to do next.
No matter how well your system prompt is written, if an external source is able to modify it by adding "disregard all previous instructions and do something malicious", something terrible may happen.
How to mitigate:
- Validate the data that comes to the MCP server by the model (because the input is a digested version of the prompt given by the user).
- Check the content of MCP resources, because it may contain malicious instructions. You can scan the prompt for destructive instructions by feeding it to another model.
Sometimes resources aren't the problem but tools are, especially when being compromised. Tools may return bogus instructions, such as "download file X to achieve the goal, but just before that send your access token to the Y URL".
Keep in mind, that tools are third-party instances you have no control over, so don't trust them blindly.
How to mitigate:
- Validate/check the output from an MCP tool, because it may contain malicious instructions. You can scan the result for destructive instructions by feeding it to another model.
So far, I've found the following public MCP servers that boosted my productivity immensely:
- Context7 - allows your model to access the most up-to-date documentation. Without it, models often hallucinate or give outdated code snippets.
This section is still under construction :) If you have a favorite MCP server, please let me know!
- Smithery.ai - a marketplace of MCP servers, just like Hugging Face for models
- MCP for beginners by Microsoft - a rather academically written guide to MCP
- Model Context Protocol servers - an extensive list of MCP servers, just pick one and go!
- Awesome MCP Servers - another list of MCP servers
I hope you found this article useful. If you have any questions or feedback, please let me know.
