I create modern web applications and custom digital tools to help businesses grow through technological innovation. My passion is combining computer science and economics to generate real value.
My passion for computer science was born at the Technical Commercial Institute of Maglie, where I discovered the power of programming and the fascination of creating digital solutions. From the start, I understood that computer science was not just code, but an extraordinary tool for turning ideas into reality.
During my studies in Business Information Systems, I began to interweave computer science and economics, understanding how technology can be the engine of growth for any business. This vision accompanied me to the University of Bari, where I obtained my degree in Computer Science, deepening my technical skills and passion for software development.
Today I put this experience at the service of businesses, professionals and startups, creating tailor-made digital solutions that automate processes, optimize resources and open new business opportunities. Because true innovation begins when technology meets the real needs of people.
My Skills
Data Analysis & Predictive Models
I transform data into strategic insights with in-depth analysis and predictive models for informed decisions
Process Automation
I create custom tools that automate repetitive operations and free up time for value-added activities
Custom Systems
I develop tailor-made software systems, from platform integrations to customized dashboards
Credo fermamente che l'informatica sia lo strumento più potente per trasformare le idee in realtà e migliorare la vita delle persone.
Democratizzare la Tecnologia
La mia missione è rendere l'informatica accessibile a tutti: dalle piccole imprese locali alle startup innovative, fino ai professionisti che vogliono digitalizzare la propria attività. Ogni realtà merita di sfruttare le potenzialità del digitale.
Unire Informatica ed Economia
Non è solo questione di scrivere codice: è capire come la tecnologia possa generare valore reale. Intrecciando competenze informatiche e visione economica, aiuto le attività a crescere, ottimizzare processi e raggiungere nuovi traguardi di efficienza e redditività.
Creare Soluzioni su Misura
Ogni attività è unica, e così devono esserlo le soluzioni. Sviluppo strumenti personalizzati che rispondono alle esigenze specifiche di ciascun cliente, automatizzando processi ripetitivi e liberando tempo per ciò che conta davvero: far crescere il business.
Trasforma la Tua Attività con la Tecnologia
Che tu gestisca un negozio, uno studio professionale o un'azienda, posso aiutarti a sfruttare le potenzialità dell'informatica per lavorare meglio, più velocemente e in modo più intelligente.
Bari, Puglia, Italy · Hybrid
Analysis and development of computer systems through the use of Java and Quarkus in Health and Public Sector. Continuous training on modern technologies for creating customized and efficient software solutions and on agents.
💼
06/2022 - 12/2024
Software analyst and Back End Developer Associate Consultant
Links Management and Technology SpA
Experience analyzing as-is software systems and ETL flows using PowerCenter. Completed Spring Boot training for developing modern and scalable backend applications. Backend developer specialized in Spring Boot, with experience in database design, analysis, development and testing of assigned tasks.
💼
02/2021 - 10/2021
Software programmer
Adesso.it (prima era WebScience srl)
Experience in AS-IS and TO-BE analysis, SEO evolutions and website evolutions to improve user performance and engagement.
🎓
2018 - 2025
Degree in Computer Science
University of Bari Aldo Moro
Bachelor's degree in Computer Science, focusing on software engineering, algorithms, and modern development practices.
📚
2013 - 2018
Diploma - Corporate Information Systems
Technical Commercial Institute of Maglie
Technical diploma specializing in Business Information Systems, combining IT knowledge with business management.
Contattami
Hai un progetto in mente? Parliamone! Compila il form qui sotto e ti risponderò al più presto.
* Campi obbligatori. I tuoi dati saranno utilizzati solo per rispondere alla tua richiesta.
Introduction: From Server to MCP Client
In previous articles we built MCP servers that expose tools, resources, and prompts. But who
consumes these capabilities? The MCP Client is the software component that
connects to one or more servers, discovers available tools, and invokes them on behalf of a language model.
In this article we will explore how to build a programmatic MCP client in TypeScript, compare the
two main transport mechanisms (STDIO and Streamable HTTP),
analyze session management, and see how to integrate an MCP server into an Express web application.
All code is available in the
Tech-MCP repository.
What You Will Learn in This Article
How to create an MCP client with the official TypeScript SDK
The tool-use cycle: query, AI decision, invocation, response
STDIO transport: local connection via child process
Streamable HTTP transport: independent server accessible over the network
Session management with session IDs and reconnection
Multi-server pattern and InMemoryTransport for testing
HTTP transport security: Origin validation, Bearer authentication
Client Project Setup
To create an MCP client from scratch, we start with a TypeScript project setup. The
@modelcontextprotocol/sdk package provides everything needed for the client side,
while @anthropic-ai/sdk allows integrating Claude as the decision-making model.
The STDIO transport is the simplest mechanism for connecting a client to an MCP server.
The client launches the server as a child process and communicates via standard input/output.
Each message is a JSON-RPC object separated by newline, while stderr remains available
for logging.
STDIO Client Architecture
The flow of a STDIO connection follows these steps:
The client creates a StdioClientTransport specifying the server command and arguments
The connect() method launches the child process and performs the initialize / initialized handshake
The client discovers available tools with listTools()
For each user query, the client invokes Claude with the tool list and manages the tool-use cycle
Complete STDIO Client Implementation
Here is the complete code for an interactive CLI client that uses Claude as the decision-making model:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import Anthropic from "@anthropic-ai/sdk";
import { config } from "dotenv";
import * as readline from "node:readline";
config(); // Load variables from .env
// Type for Anthropic API compatible tools
interface AnthropicTool {
name: string;
description: string | undefined;
input_schema: Record<string, unknown>;
}
class McpChatClient {
private client: Client;
private anthropic: Anthropic;
private tools: AnthropicTool[] = [];
constructor() {
this.client = new Client({
name: "mcp-chat-client",
version: "1.0.0",
});
this.anthropic = new Anthropic();
}
/**
* Connect to MCP server via STDIO.
* The client launches the server as a child process.
*/
async connect(serverCommand: string, serverArgs: string[]): Promise<void> {
const transport = new StdioClientTransport({
command: serverCommand,
args: serverArgs,
});
// connect() performs the lifecycle: initialize -> initialized
await this.client.connect(transport);
// Discover available tools
const result = await this.client.listTools();
this.tools = result.tools.map((tool) => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema as Record<string, unknown>,
}));
console.error(
`Connected. Available tools:
#123;this.tools.map((t) => t.name).join(", ")}`
);
}
/**
* Process a user query with Claude's tool-use cycle.
*/
async chat(userMessage: string): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userMessage },
];
let response = await this.anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
messages,
tools: this.tools as Anthropic.Tool[],
});
// Tool-use cycle: Claude may request multiple tools in sequence
while (response.stop_reason === "tool_use") {
const assistantContent = response.content;
messages.push({ role: "assistant", content: assistantContent });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of assistantContent) {
if (block.type === "tool_use") {
console.error(` -> Invoking: #123;block.name}`);
// Call the tool on the MCP server
const result = await this.client.callTool({
name: block.name,
arguments: block.input as Record<string, unknown>,
});
const text = (result.content as Array<{ type: string; text: string }>)
.map((c) => c.text)
.join("\n");
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: text,
});
}
}
messages.push({ role: "user", content: toolResults });
response = await this.anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
messages,
tools: this.tools as Anthropic.Tool[],
});
}
return response.content
.filter((block): block is Anthropic.TextBlock => block.type === "text")
.map((block) => block.text)
.join("\n");
}
async disconnect(): Promise<void> {
await this.client.close();
}
}
The Tool-Use Flow in Detail
The heart of the MCP client is the tool-use cycle, an iterative mechanism that allows the AI
to autonomously decide which tools to invoke to answer a query. Here is the complete flow:
The client sends the user query to Claude along with the tool list (names, descriptions, JSON schemas)
Claude analyzes the query and decides whether to use a tool (responds with stop_reason: "tool_use")
The client invokes the tool on the MCP server with client.callTool()
The result goes back to Claude as tool_result
Claude may request additional tools or generate the final response (stop_reason: "end_turn")
STDIO vs Streamable HTTP Transport
The MCP SDK supports two fundamentally different transport mechanisms. The choice between them depends
on the use case, system architecture, and deployment requirements.
STDIO vs Streamable HTTP Comparison
Aspect
STDIO
Streamable HTTP
Architecture
Client launches server as child process
Server is an independent service
Connections
1:1 (one client per process)
N:1 (many clients, one server)
Deployment
Local only, same host
Local or remote, any network
Sessions
Implicit (process lifetime)
Explicit (session ID in header)
Authentication
Not needed (same host)
Required (Bearer token, OAuth)
Scalability
Limited (one process per client)
High (one server, many clients)
Use case
Claude Desktop, IDEs, local development
Microservices, shared APIs, production
Setup complexity
Minimal
Requires HTTP server (Express, Fastify)
In general, STDIO is the best choice for local development and integration with
desktop applications like Claude Desktop and Cursor. Streamable HTTP is necessary when
the server needs to be accessible from multiple clients, remote hosts, or in containerized production environments.
MCP Server with HTTP Transport (Express)
To expose an MCP server via HTTP, the SDK provides StreamableHTTPServerTransport which
integrates natively with Express. The server becomes an independent service that accepts connections
from any compatible client.
Dependency Setup
npm install express
npm install -D @types/express
HTTP Server Implementation
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport }
from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { randomUUID } from "node:crypto";
import { z } from "zod";
// Create the MCP server (same API as STDIO)
const server = new McpServer({
name: "notes-http-server",
version: "1.0.0",
});
// Register tools
const notes: Map<string, string> = new Map();
server.tool(
"add-note",
"Adds a new note",
{
title: z.string(),
content: z.string(),
},
async ({ title, content }) => {
notes.set(title, content);
return {
content: [{ type: "text", text: `Note "#123;title}" saved.` }],
};
},
);
// --- HTTP Transport with Express ---
const app = express();
app.use(express.json());
// Create transport with session generator
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
// Connect MCP server to transport
await server.connect(transport);
// MCP endpoint: POST (client -> server messages)
app.post("/mcp", async (req, res) => {
await transport.handleRequest(req, res, req.body);
});
// MCP endpoint: GET (SSE stream server -> client)
app.get("/mcp", async (req, res) => {
await transport.handleRequest(req, res);
});
// MCP endpoint: DELETE (session termination)
app.delete("/mcp", async (req, res) => {
await transport.handleRequest(req, res);
});
// Health check
app.get("/health", (_req, res) => {
res.json({ status: "ok", server: "notes-http-server" });
});
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => {
console.log(`MCP HTTP Server at http://localhost:#123;PORT}/mcp`);
});
Endpoints and HTTP Protocol
The server exposes a single path /mcp with three HTTP methods, each with a specific role
in the MCP protocol:
POST /mcp - The client sends JSON-RPC messages (request, notification, response). This is the main channel for tool invocation.
GET /mcp - The client opens an SSE (Server-Sent Events) stream to receive real-time push notifications from the server.
DELETE /mcp - The client explicitly terminates the session, freeing server-side resources.
Fundamental Difference from STDIO
With STDIO, each connection creates a new server process. With HTTP, the server connects to the transport
only once with server.connect(transport) and handles all sessions
concurrently. The sessionIdGenerator assigns a unique UUID to each connecting client.
MCP Client with HTTP Transport
To connect to an HTTP server, the SDK provides StreamableHTTPClientTransport. The client
API remains identical to STDIO: once the connection is established, listTools(),
callTool(), and close() work exactly the same way.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport }
from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({
name: "http-client",
version: "1.0.0",
});
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:3000/mcp"),
);
await client.connect(transport);
// From here on, same API as STDIO
const tools = await client.listTools();
console.log("Available tools:", tools.tools.map((t) => t.name));
const result = await client.callTool({
name: "add-note",
arguments: { title: "Test", content: "Test content" },
});
console.log("Result:", result);
await client.close();
The fundamental aspect is that the application code is identical regardless of the transport.
The choice between STDIO and HTTP is an infrastructural decision, not an architectural one: the client's
business logic remains unchanged.
Session Management
Session management is one of the most important aspects of the HTTP transport. While in STDIO the session
is implicit (it coincides with the process lifetime), in HTTP each client connection receives a
unique session ID that must be included in all subsequent requests.
How Sessions Work
The client sends the first initialize request without a session ID
The server generates a UUID with sessionIdGenerator and returns it in the Mcp-Session-Id header
The client includes Mcp-Session-Id in all subsequent requests
The server associates each request with the correct session and maintains state
On disconnection, the client sends DELETE /mcp to terminate the session
Sending the Mcp-Session-Id header after initialization
Sending the MCP-Protocol-Version header in all requests
Reconnection in case of expired session (HTTP 404)
Stateful Pattern: Per-Session State
In production, each session can have its own isolated state. Here is how to implement it:
import { randomUUID } from "node:crypto";
// Map sessions -> isolated state
const sessions = new Map<string, { notes: Map<string, string> }>();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => {
const sessionId = randomUUID();
sessions.set(sessionId, { notes: new Map() });
return sessionId;
},
});
Connecting to Multiple Servers
An MCP client can connect to multiple servers simultaneously, aggregating available
tools into a single list. This pattern is essential in complex architectures where tools are
distributed across specialized servers.
class MultiServerClient {
private clients: Map<string, Client> = new Map();
private allTools: AnthropicTool[] = [];
async addServer(
name: string, command: string, args: string[]
): Promise<void> {
const client = new Client({
name: `client-#123;name}`,
version: "1.0.0"
});
const transport = new StdioClientTransport({ command, args });
await client.connect(transport);
const result = await client.listTools();
for (const tool of result.tools) {
this.allTools.push({
// Prefix to avoid name collisions
name: `#123;name}__#123;tool.name}`,
description: `[#123;name}] #123;tool.description}`,
input_schema: tool.inputSchema as Record<string, unknown>,
});
}
this.clients.set(name, client);
}
async callTool(
prefixedName: string, args: Record<string, unknown>
): Promise<unknown> {
const [serverName, toolName] = prefixedName.split("__");
const client = this.clients.get(serverName);
if (!client) throw new Error(`Server #123;serverName} not connected`);
return client.callTool({ name: toolName, arguments: args });
}
}
The serverName__toolName prefix is essential to avoid collisions when different servers
expose tools with the same name. Claude receives the complete list and the client handles the routing.
Dual Transport: Supporting STDIO and HTTP
A professional MCP server should support both transports, deciding which one
to use based on configuration. This allows using the same code both with Claude Desktop
(STDIO) and in production (HTTP).
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport }
from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { randomUUID } from "node:crypto";
function createServer(): McpServer {
const server = new McpServer({ name: "my-server", version: "1.0.0" });
// ... register tools, resources, prompts ...
return server;
}
// Conditional startup based on environment variable
const mode = process.env.MCP_TRANSPORT ?? "stdio";
if (mode === "http") {
const server = createServer();
const app = express();
app.use(express.json());
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
await server.connect(transport);
app.post("/mcp", async (req, res) =>
await transport.handleRequest(req, res, req.body));
app.get("/mcp", async (req, res) =>
await transport.handleRequest(req, res));
app.delete("/mcp", async (req, res) =>
await transport.handleRequest(req, res));
app.get("/health", (_, res) => res.json({ status: "ok" }));
const port = process.env.PORT ?? 3000;
app.listen(port, () => console.log(`HTTP on port #123;port}`));
} else {
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Server started on STDIO");
}
Transport Configuration
MCP_TRANSPORT=stdio (default) - For Claude Desktop and local IDEs
MCP_TRANSPORT=http PORT=3000 - For HTTP deployment and microservices
InMemoryTransport for Testing
For unit tests, there is no need to launch processes or HTTP servers. The SDK provides
InMemoryTransport which directly connects client and server in memory:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
async function testInMemory() {
const server = new McpServer({ name: "test-server", version: "1.0.0" });
server.tool("ping", "Test ping", {}, async () => ({
content: [{ type: "text", text: "pong" }],
}));
// Create linked transport pair
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
// IMPORTANT: server connects BEFORE the client
await server.connect(serverTransport);
const client = new Client({ name: "test-client", version: "1.0.0" });
await client.connect(clientTransport);
const result = await client.callTool({ name: "ping", arguments: {} });
console.log(result);
// { content: [{ type: "text", text: "pong" }] }
await client.close();
await server.close();
}
Critical Connection Order
With InMemoryTransport, server.connect() must be called
beforeclient.connect(). The client sends the
initialize message immediately upon connect(), and the server must
already be listening to respond.
HTTP Transport Security
When the MCP server is exposed via HTTP, security becomes a critical aspect. Here are the
fundamental measures to implement.
Origin Validation
To prevent DNS rebinding attacks, validate the Origin header of every request:
In this article we explored in depth the client side of the MCP protocol
and the two fundamental transport mechanisms. Here are the key concepts:
An MCP client connects to one or more servers, discovers tools, and invokes them on behalf of a language model
The tool-use cycle allows the AI to autonomously decide which tools to call, with the client acting as intermediary
STDIO is ideal for local development and desktop integrations (Claude Desktop, Cursor)
Streamable HTTP is necessary for remote servers, multiple connections, and production environments
Session management with session IDs ensures state isolation between different clients
The dual transport pattern allows supporting both modes with the same code
InMemoryTransport simplifies testing without processes or HTTP servers
In the next article we will dive into Shared Packages in the Tech-MCP monorepo:
the Core package with common utilities, the EventBus for inter-server communication, and the
database access modules. We will see how to structure reusable code in a professional TypeScript monorepo.