In this guide, you’ll learn how the Model Context Protocol (MCP) works and how to set it up. We’ll go through the steps of building your first server to make it all function properly.
Let’s start!
You can also check out our guide to using the OpenAI API with Python.
What is Model Context Protocol (MCP)?

Model Context Protocol (MCP) is a structured system that lets language models perform actions through external tools or services.
Instead of just responding based on trained knowledge, the model can make a request, ask for something to be done, and use the result in its reasoning.
MCP includes three main parts:
- Model: This is the language model itself, like GPT-4 or Claude.
- Context: This holds shared data between the model and tools – things like temporary state, memory, or previous messages.
- Protocol: This defines how the model and tools communicate. It’s usually based on JSON.
Here’s what happens:
- The model sends a tool call request.
- The MCP server validates it.
- The server runs the linked action or service.
- A structured response goes back to the model.
Core Components of MCP
So, here’s a breakdown of what’s happening behind the scenes when a model uses MCP.
1. Input Parsing
The first step is tokenization. The model splits input into tokens – small chunks of text. Then it converts those into embeddings.
These embeddings are vectors that hold meaning based on relationships between words. That’s how the model starts to understand what you’re saying.
2. Transformers and Attention
Most MCP implementations use Transformer-based models. These models use attention mechanisms.
That means the model can weigh parts of the input differently depending on what’s most relevant.
If you say, “Remind me to email Sarah on Friday,” the model pays closer attention to “email,” “Sarah,” and “Friday.” This helps it figure out the intent of your request.
3. Memory
Some setups include memory. Short-term memory tracks what you’ve said in the current session. Long-term memory can store information across sessions – like preferences or past tasks.
Some systems also use retrieval-augmented generation (RAG). RAG pulls in information from documents or external data sources during the model’s response generation.
4. Rule-Based Logic
In addition to model predictions, some systems use fixed rules. These are built-in instructions that guide what the model can or cannot do.
For example, they might prevent tool calls with invalid inputs or stop the model from returning inappropriate outputs. These rules sit outside the model but work with it.
MCP Server Structure
An MCP server processes the model’s requests and sends back structured responses. It’s built in layers, each handling a different part of the process.
1. Tool Registry
This holds all the available tools the model can use. Each tool includes a schema that defines its expected inputs and outputs. The model uses this schema to format its requests correctly.
2. Request Handler
The handler receives tool calls from the model. It checks which tool is being requested and connects it to the right logic.
3. Service Connectors
These run the actual actions. If a tool involves sending a message on Slack or querying a database, the connector handles that part. Each connector wraps the external service behind a consistent interface.
4. Validation Layer
Before running any tool, the input is validated. Libraries like Zod are often used to make sure the input matches the expected structure. This step reduces errors and helps catch problems early.
5. Response Formatter
After a tool runs, the output is formatted so the model can read it. This might include adding metadata or structuring the result in a predictable way. The goal is to keep the interaction clean and easy for the model to use.
6. Transport Layer
This part manages the communication between the model and the server. Some systems use standard input/output streams. Others use HTTP or gRPC. The format doesn’t matter much, as long as it’s consistent and reliable.
Tool Definition Schema
Tools in MCP are defined with clear specifications that include:
interface McpTool {
name: string;
description: string;
parameters: {
type: string;
properties: Record<string, {
type: string;
description: string;
required?: boolean;
}>;
};
}
This schema ensures that language models understand what each tool does, what parameters it requires, and how to structure requests appropriately.
Communication Flow
When a language model needs to use an external tool via MCP:
- The model generates a structured tool call based on the tool’s schema
- The MCP server receives this call via the transport layer
- The server validates the request and parameters
- The appropriate service connector executes the request
- Results are formatted and sent back to the model
- The model incorporates these results into its response
This standardized flow works across different types of tools and services, creating consistency in how LLMs interact with external systems.
How MCP Works?
Request/Response Cycle
The technical details of MCP involve a specific request/response cycle:
- Tool Discovery: The model receives information about available tools
- Request Formation: The model formats a request with required parameters
- Request Transmission: The request is sent via the transport mechanism (often standard I/O)
- Parameter Validation: The server validates all parameters against schemas
- Service Execution: The server calls the appropriate external service
- Response Generation: Results are formatted according to the protocol
- Response Transmission: Data is sent back to the model
- Response Integration: The model incorporates the data into its reasoning
Message Format Specifications
MCP messages follow a specific JSON structure:
{
"type": "function_call",
"tool": "tool_name",
"parameters": {
"param1": "value1",
"param2": "value2"
}
}
Responses use a similar structure:
{
"type": "function_response",
"tool": "tool_name",
"result": {
"status": "success",
"data": { ... }
}
}
This structured approach enables consistency and facilitates validation at each step.
Authentication and Security
MCP servers implement several security mechanisms:
- API credentials stored as environment variables
- No hardcoding of secrets in the codebase
- Request validation to prevent injection attacks
- Rate limiting to protect external services
- Transport layer security for data protection
These measures protect both the MCP server and the external services it connects to.
Error Handling
Robust error handling is crucial for MCP implementations:
try {
const result = await externalService.callApi(params);
return { status: "success", data: result };
} catch (error) {
return {
status: "error",
error: {
code: error.code || "UNKNOWN_ERROR",
message: error.message || "An unknown error occurred"
}
};
}
MCP vs. Traditional API Approaches
Traditional API Integration | Model Context Protocol (MCP) |
---|---|
Custom connectors per service | One protocol across services and models |
Inconsistent parameter formats | Standardized tool definitions |
Model-specific implementations | Model-agnostic structure |
Poor portability across LLMs | Reusable across LLMs and tools |
Slower development, more maintenance | Faster setup and easier to maintain |
Building an MCP Server: Essential Components
Server Skeleton
A basic MCP server starts with this structure:
import { createMcpServer } from '@modelcontextprotocol/sdk';
import { z } from 'zod';
const server = createMcpServer();
// Register tools
server.registerTool({
name: 'example_tool',
description: 'An example tool that demonstrates MCP functionality',
parameters: z.object({
input: z.string().describe('The input to process')
}),
handler: async ({ input }) => {
// Tool implementation
return { processed: input.toUpperCase() };
}
});
// Start the server
server.start();
Tool Definition Best Practices
When defining tools:
- Use clear, descriptive names
- Provide detailed descriptions of what the tool does
- Define all parameters with accurate types
- Include examples where helpful
- Make error messages informative
- Keep tools focused on a single responsibility
These practices make tools more usable for language models.
Input/Output Validation
Libraries like Zod provide strong validation:
const taskSchema = z.object({
title: z.string().min(1, "Title is required"),
description: z.string().optional(),
dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD"),
priority: z.enum(["low", "normal", "high", "urgent"]).optional()
});
This validation prevents malformed requests from reaching external services and ensures consistent data formats.
State Management
For tools that require state persistence:
class SessionManager {
private sessions: Map<string, SessionData> = new Map();
createSession(userId: string): string {
const sessionId = generateUniqueId();
this.sessions.set(sessionId, { userId, createdAt: new Date(), data: {} });
return sessionId;
}
getSession(sessionId: string): SessionData | undefined {
return this.sessions.get(sessionId);
}
updateSessionData(sessionId: string, data: Record<string, any>): boolean {
const session = this.sessions.get(sessionId);
if (!session) return false;
session.data = { ...session.data, ...data };
return true;
}
}
This pattern allows tools to maintain context across multiple interactions.
MCP Integration – ClickUp Task Management
A practical MCP server for ClickUp might implement these tools:
- Get Task Details: Retrieves information about a specific task
- Create Task: Creates a new task with specified parameters
- Update Task Status: Changes the status of an existing task
- List Tasks: Retrieves a filtered list of tasks
Implementation highlights:
server.registerTool({
name: 'clickup_get_task',
description: 'Retrieve details about a ClickUp task by ID',
parameters: z.object({
taskId: z.string().describe('The ID of the task to retrieve')
}),
handler: async ({ taskId }) => {
const clickupService = new ClickUpService(process.env.CLICKUP_API_TOKEN);
return await clickupService.getTask(taskId);
}
});
server.registerTool({
name: 'clickup_create_task',
description: 'Create a new task in ClickUp',
parameters: z.object({
listId: z.string().describe('The list ID where the task will be created'),
name: z.string().describe('The name of the task'),
description: z.string().optional().describe('Task description'),
priority: z.number().min(1).max(4).optional().describe('Priority (1-4)'),
dueDate: z.number().optional().describe('Due date timestamp')
}),
handler: async (params) => {
const clickupService = new ClickUpService(process.env.CLICKUP_API_TOKEN);
return await clickupService.createTask(params);
}
});
This implementation allows language models to interact directly with ClickUp’s task management system.
Database Operations
MCP can also connect LLMs to databases:
server.registerTool({
name: 'db_query',
description: 'Execute a read-only SQL query on the database',
parameters: z.object({
query: z.string().describe('SQL SELECT query to execute'),
parameters: z.array(z.any()).optional().describe('Query parameters')
}),
handler: async ({ query, parameters = [] }) => {
// Validate query is read-only
if (!/^SELECT/i.test(query)) {
throw new Error('Only SELECT queries are allowed');
}
const dbService = new DatabaseService();
const results = await dbService.executeQuery(query, parameters);
return { rowCount: results?.length || 0, rows: results };
}
});
This pattern allows language models to retrieve information from databases without direct access to the database itself.
Document Processing Systems
For document management:
server.registerTool({
name: 'document_search',
description: 'Search for documents matching specific criteria',
parameters: z.object({
query: z.string().describe('Search query'),
filters: z.object({
docType: z.array(z.string()).optional(),
dateRange: z.object({
start: z.string().optional(),
end: z.string().optional()
}).optional(),
tags: z.array(z.string()).optional()
}).optional()
}),
handler: async ({ query, filters = {} }) => {
const documentService = new DocumentService();
return await documentService.searchDocuments(query, filters);
}
});
This implementation allows LLMs to search and retrieve documents based on complex criteria.
IoT Device Control
MCP can enable LLMs to control IoT devices:
server.registerTool({
name: 'iot_device_control',
description: 'Control an IoT device',
parameters: z.object({
deviceId: z.string().describe('The ID of the device to control'),
command: z.enum(['on', 'off', 'status']).describe('Command to send'),
parameters: z.record(z.any()).optional().describe('Additional parameters')
}),
handler: async ({ deviceId, command, parameters = {} }) => {
const iotService = new IoTService();
return await iotService.sendCommand(deviceId, command, parameters);
}
});
Advanced MCP Patterns
Streaming Responses
For handling large datasets or long-running operations:
server.registerStreamingTool({
name: 'data_stream',
description: 'Stream large datasets in chunks',
parameters: z.object({
datasetId: z.string().describe('ID of the dataset to stream')
}),
handler: async function* ({ datasetId }) {
const dataService = new DataService();
const stream = await dataService.getDataStream(datasetId);
for await (const chunk of stream) {
yield { chunk };
}
}
});
This pattern allows progressive delivery of large results without overwhelming the language model.
Chaining Multiple MCP Tools
Complex operations often require multiple tool calls:
// Tool 1: Find a document
const documentId = await mcpCall('document_search', { query: 'quarterly report' });
// Tool 2: Extract data from the document
const financialData = await mcpCall('document_extract', {
documentId,
extractionType: 'financial_metrics'
});
// Tool 3: Analyze the data
const analysis = await mcpCall('financial_analysis', { data: financialData });
This chaining pattern allows language models to perform multi-step operations across different services.
Context-Aware Tools
Tools can maintain awareness of the conversation context:
server.registerTool({
name: 'context_aware_search',
description: 'Search with awareness of previous context',
parameters: z.object({
query: z.string().describe('Search query'),
sessionId: z.string().describe('Session identifier for context'),
}),
handler: async ({ query, sessionId }) => {
const sessionManager = SessionManager.getInstance();
const session = sessionManager.getSession(sessionId);
if (!session) throw new Error('Invalid session');
const searchService = new SearchService();
const results = await searchService.searchWithContext(
query,
session.previousQueries || []
);
// Update session with this query
session.previousQueries = [
...(session.previousQueries || []),
query
];
return results;
}
});
This approach enables more natural and coherent interactions across multiple requests.
Debugging and Testing MCP Servers
Unit Testing Strategies
Effective unit tests for MCP tools:
describe('clickup_create_task', () => {
it('should create a task with valid parameters', async () => {
// Mock the ClickUp service
jest.spyOn(ClickUpService.prototype, 'createTask')
.mockResolvedValue({ id: 'abc123', name: 'Test Task' });
// Call the tool
const result = await callTool('clickup_create_task', {
listId: 'list123',
name: 'Test Task',
description: 'Task description',
priority: 2
});
// Assert
expect(result).toHaveProperty('id', 'abc123');
expect(ClickUpService.prototype.createTask).toHaveBeenCalledWith(
expect.objectContaining({
listId: 'list123',
name: 'Test Task'
})
);
});
it('should reject invalid parameters', async () => {
await expect(
callTool('clickup_create_task', {
listId: 'list123',
// Missing required 'name' parameter
priority: 5 // Invalid priority (out of range)
})
).rejects.toThrow();
});
});
These tests verify both successful operation and proper error handling.
Integration Testing
For testing complete flows:
describe('Task Management Flow', () => {
it('should create and then retrieve a task', async () => {
// Create a task
const createResult = await callTool('clickup_create_task', {
listId: 'list123',
name: 'Integration Test Task'
});
expect(createResult).toHaveProperty('id');
const taskId = createResult.id;
// Retrieve the task
const getResult = await callTool('clickup_get_task', {
taskId
});
expect(getResult).toHaveProperty('name', 'Integration Test Task');
});
});
These tests verify that multiple tools work together correctly.
Using the MCP Inspector
The MCP Inspector is a valuable tool for testing:
npx @modelcontextprotocol/inspector --server-command="node dist/index.js"
This tool allows you to:
- See available tools and their schemas
- Make test calls to tools
- View detailed response information
- Debug errors in real-time
Common Failure Patterns
Watch for these common issues:
Issue | Description |
---|---|
Parameter validation failures | Ensure all parameters conform to schemas |
Authentication errors | Check API tokens and credentials |
Timeout issues | Add appropriate timeouts for external services |
Rate limiting | Implement exponential backoff for retry logic |
Response format mismatches | Ensure responses match what models expect |
Deployment Strategies for Production
Docker Containerization
A production-ready Dockerfile for MCP servers:
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --production
ENV NODE_ENV=production
CMD ["node", "dist/index.js"]
This multi-stage build creates a minimal production image.
Serverless MCP
For serverless deployments, consider:
// AWS Lambda handler
export const handler = async (event) => {
// Parse the incoming MCP request
const mcpRequest = JSON.parse(event.body);
// Initialize tools
const tools = initializeTools();
// Find and execute the requested tool
const tool = tools.find(t => t.name === mcpRequest.tool);
if (!tool) {
return {
statusCode: 404,
body: JSON.stringify({ error: 'Tool not found' })
};
}
try {
const result = await tool.handler(mcpRequest.parameters);
return {
statusCode: 200,
body: JSON.stringify({ result })
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: error.message })
};
}
};
This pattern adapts MCP to serverless environments like AWS Lambda.
Multi-Region Deployment
For optimal performance globally:
- Deploy MCP servers in multiple regions
- Use geo-routing to direct requests to the nearest server
- Implement global state synchronization if needed
- Monitor region-specific performance metrics
- Implement failover mechanisms between regions
High Availability Setup
To ensure reliability:
- Deploy multiple instances of each MCP server
- Use container orchestration like Kubernetes for management
- Implement health checks and automatic restarts
- Set up load balancing across instances
- Use redundant external service connections
Security Considerations for MCP
Authentication Best Practices
Secure your MCP servers with:
// Middleware for authentication
server.use(async (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey || !isValidApiKey(apiKey)) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Add rate limiting here if needed
next();
});
Managing API Credentials
For secure credential management:
- Use environment variables for secrets
- Implement credential rotation schedules
- Use a secrets manager service where appropriate
- Create service-specific credentials with minimal permissions
- Audit credential usage regularly
Input Validation and Sanitization
Beyond schema validation, implement:
function sanitizeInput(input: string): string {
// Remove potentially dangerous patterns
return input
.replace(/[<>]/g, '') // Remove HTML tags
.replace(/[\r\n]+/g, ' ') // Normalize line endings
.trim();
}
server.registerTool({
name: 'example_tool',
// ...
handler: async ({ input }) => {
const sanitizedInput = sanitizeInput(input);
// Process with sanitized input
}
});
Rate Limiting
Protect external services with rate limiting:
class RateLimiter {
private requests: Map<string, number[]> = new Map();
private limit: number;
private window: number;
constructor(limit = 10, windowMs = 60000) {
this.limit = limit;
this.window = windowMs;
}
allowRequest(key: string): boolean {
const now = Date.now();
const windowStart = now - this.window;
// Get or initialize requests for this key
const requests = this.requests.get(key) || [];
// Filter out requests outside the current window
const recentRequests = requests.filter(time => time > windowStart);
// Check if we've hit the limit
if (recentRequests.length >= this.limit) {
return false;
}
// Add the current request
recentRequests.push(now);
this.requests.set(key, recentRequests);
return true;
}
}
Performance Optimization Techniques
Response Time Improvements
To improve responsiveness:
- Implement connection pooling for database or API connections
- Use parallel processing where appropriate
- Optimize query patterns for external services
- Implement timeouts to prevent long-running operations
- Monitor and optimize the most frequently used tools
Caching Implementation
Effective caching can dramatically improve performance:
class CacheService {
private cache: Map<string, { data: any, expiry: number }> = new Map();
async getOrSet<T>(
key: string,
fetchFn: () => Promise<T>,
ttlMs = 60000
): Promise<T> {
const now = Date.now();
const cached = this.cache.get(key);
// Return cached value if valid
if (cached && cached.expiry > now) {
return cached.data;
}
// Fetch fresh data
const data = await fetchFn();
// Cache the result
this.cache.set(key, {
data,
expiry: now + ttlMs
});
return data;
}
invalidate(key: string): void {
this.cache.delete(key);
}
}
Request Batching
For multiple similar operations:
server.registerTool({
name: 'batch_get_users',
description: 'Get multiple users in a single request',
parameters: z.object({
userIds: z.array(z.string()).describe('Array of user IDs to retrieve')
}),
handler: async ({ userIds }) => {
const userService = new UserService();
// Single request to get multiple users
return await userService.batchGetUsers(userIds);
}
});
Enterprise MCP Implementation
Multi-Tenant Architecture
For enterprise deployments:
class TenantRouter {
private tenantConfigs: Map<string, TenantConfig> = new Map();
registerTenant(tenantId: string, config: TenantConfig): void {
this.tenantConfigs.set(tenantId, config);
}
getTenantConfig(tenantId: string): TenantConfig | undefined {
return this.tenantConfigs.get(tenantId);
}
async routeRequest(tenantId: string, tool: string, params: any): Promise<any> {
const config = this.getTenantConfig(tenantId);
if (!config) {
throw new Error(`Unknown tenant: ${tenantId}`);
}
// Use tenant-specific configuration for the request
const service = new ServiceFactory(config).createService(tool);
return await service.execute(params);
}
}
Practical Tutorial: Build Your First MCP Server
Let’s walk through building a simple but complete MCP server that connects to a weather API:
Step 1: Project Setup
mkdir weather-mcp-server
cd weather-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod dotenv axios
npm install --save-dev typescript ts-node @types/node
Create a tsconfig.json
file:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"outDir": "./dist",
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Step 2: Create the Weather Service
Create src/services/weather-service.ts
:
import axios from 'axios';
export class WeatherService {
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async getCurrentWeather(location: string) {
try {
const response = await axios.get(
`https://api.weatherapi.com/v1/current.json`,
{
params: {
key: this.apiKey,
q: location
}
}
);
const { current, location: locationData } = response.data;
return {
location: {
name: locationData.name,
region: locationData.region,
country: locationData.country
},
temperature: {
celsius: current.temp_c,
fahrenheit: current.temp_f
},
condition: current.condition.text,
windSpeed: {
kph: current.wind_kph,
mph: current.wind_mph
},
humidity: current.humidity,
updated: current.last_updated
};
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
throw new Error(`Weather API error: ${error.response.data.error.message}`);
}
throw new Error(`Failed to get weather: ${error.message}`);
}
}
async getForecast(location: string, days: number = 3) {
try {
const response = await axios.get(
`https://api.weatherapi.com/v1/forecast.json`,
{
params: {
key: this.apiKey,
q: location,
days: days
}
}
);
const { forecast, location: locationData } = response.data;
return {
location: {
name: locationData.name,
region: locationData.region,
country: locationData.country
},
forecast: forecast.forecastday.map(day => ({
date: day.date,
maxTemp: {
celsius: day.day.maxtemp_c,
fahrenheit: day.day.maxtemp_f
},
minTemp: {
celsius: day.day.mintemp_c,
fahrenheit: day.day.mintemp_f
},
condition: day.day.condition.text,
chanceOfRain: day.day.daily_chance_of_rain
}))
};
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
throw new Error(`Weather API error: ${error.response.data.error.message}`);
}
throw new Error(`Failed to get forecast: ${error.message}`);
}
}
}
Step 3: Define the Tools
Create src/controllers/weather-controller.ts
:
import { z } from 'zod';
import { WeatherService } from '../services/weather-service';
export const getCurrentWeatherTool = {
name: 'get_current_weather',
description: 'Get current weather conditions for a location',
parameters: z.object({
location: z.string().describe('Location name, city, ZIP code, or coordinates')
}),
handler: async ({ location }) => {
const weatherService = new WeatherService(process.env.WEATHER_API_KEY!);
return await weatherService.getCurrentWeather(location);
}
};
export const getWeatherForecastTool = {
name: 'get_weather_forecast',
description: 'Get weather forecast for a location',
parameters: z.object({
location: z.string().describe('Location name, city, ZIP code, or coordinates'),
days: z.number().min(1).max(7).default(3).describe('Number of days to forecast (1–7)')
}),
handler: async ({ location, days }) => {
const weatherService = new WeatherService(process.env.WEATHER_API_KEY!);
return await weatherService.getForecast(location, days);
}
};
Step 4: Create the Main Server File
Create src/index.ts
:
import { createMcpServer } from '@modelcontextprotocol/sdk';
import { config } from 'dotenv';
import { getCurrentWeatherTool, getWeatherForecastTool } from './controllers/weather-controller';
config();
if (!process.env.WEATHER_API_KEY) {
console.error('Error: WEATHER_API_KEY environment variable is required');
process.exit(1);
}
const server = createMcpServer();
server.registerTool(getCurrentWeatherTool);
server.registerTool(getWeatherForecastTool);
server.start();
console.log('Weather MCP server started');
Step 5: Set Up Environment Variables
Create a .env
file:
Step 6: Add Build Scripts
Update package.json
:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts"
}
}
Step 7: Build and Run
# For development
npm run dev
# For production
npm run build
npm start
Testing with the MCP Inspector
npx @modelcontextprotocol/inspector --server-command="npm run dev"
You can:
- View the available tools
- Test
get_current_weather
with different locations - Test
get_weather_forecast
with different parameters - See formatted responses
Common Pitfalls and Solutions
Issue | What to Check |
---|---|
Authentication Issues | Confirm your API key is valid and loaded correctly |
Parameter Validation | Make sure input matches the expected schema |
Error Handling | Throw useful error messages |
Rate Limiting | Add exponential backoff or throttling |
Testing | Try various inputs and edge cases |
What’s Next?
Now that MCP is open, you don’t need to build separate connectors for every tool. Just plug into the standard. Claude Desktop already supports local MCP servers, and there are open-source ones ready for GitHub, Slack, Postgres, and more.
If you’re working inside a company, you can start testing it against internal systems right away. This cuts the glue work and lets you move faster when connecting models to real data.
Good Luck!
No Comment! Be the first one.