MCP (Model Context Protocol) är implementerat av flera agentklienter och utvecklingsverktyg. Den spridningen gör protokollet till en användbar integrationsgräns, men den gör ingen server portabel, säker eller produktionsklar utan test mot klienterna och mot en hotmodell.
Om du vill att LLM-agenter ska interagera med din tjänst är en MCP-server lösningen. När du har byggt en eller två ser du att själva protokollet är litet. Det intressanta ingenjörsarbetet finns i allt runt omkring: schemadesign, felhantering, autentisering, strömning, prestanda och observerbarhet.
Artikeln har två medvetet åtskilda resultat: en minimal stdio-server som du kan köra, och en designchecklista för produktion. Den påstår inte att fragmenten tillsammans utgör en driftsatt, autentiserad tjänst. Den minimala koden är skriven mot @modelcontextprotocol/sdk@1.30.0. Den aktuella v2-SDK:n använder uppdelade paket (@modelcontextprotocol/server, @modelcontextprotocol/node och ramverksadaptrar), så följ den officiella serverguiden när du påbörjar nytt v2-arbete. Blanda inte v1- och v2-importer.
Kort om vad MCP är
MCP är ett klient–server-protokoll där:
- Servrar exponerar verktyg, resurser och prompter.
- Klienter vanligtvis är LLM-agenter som använder dem.
Protokollet använder JSON-RPC 2.0. Läs den officiella specifikationen för den valda revisionen. Transporterna är stdio för lokala processer och Streamable HTTP för fjärrservrar – den äldre transporten HTTP+SSE utfasades i specifikationsrevisionen 2025-03-26. Specifikationen för HTTP-behörighet bygger på OAuth. Applikationsgateways kan lägga till andra autentiseringsscheman, men en API-nyckel ersätter inte efterlevnad av MCP:s behörighetsspecifikation och får inte framställas som det.
Serverns uppgift är att exponera användbara funktioner för LLM:er på ett sätt som gör att de kan upptäcka och använda dem.
Grundstrukturen
Skapa en ren katalog och nagla fast samma huvudversion som i det här exemplet:
npm init -y
npm install @modelcontextprotocol/sdk@1.30.0 zod@3
npm install --save-dev typescript@5 @types/node
Lägg sedan till den här minimala servern med v1:s högnivå-API McpServer:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
server.registerTool(
"echo",
{
description: "Returnera den angivna texten.",
inputSchema: { text: z.string() },
},
async ({ text }) => ({
content: [{ type: "text", text }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
(Samma SDK exponerar också lågnivåklassen Server tillsammans med setRequestHandler för ListToolsRequestSchema/CallToolRequestSchema om du vill ha full kontroll över hanterarna för begäranden – men för de flesta servrar är McpServer.registerTool kortare och svårare att göra fel med.)
Det är stommen. Det är innehållet i verktygshanterarna – och hur du bygger dem – som är själva arbetet.
Mönster 1: Filosofi för verktygsdesign
Det första beslutet är vilka verktyg du exponerar och på vilken detaljnivå.
Ett vanligt fel är att mekaniskt exponera varje underliggande slutpunkt som ett verktyg. En stor uppsättning irrelevanta verktyg ökar schemakontexten och kan öka antalet felval för vissa modeller och uppgifter. Jämför uppgiftsinriktade verktyg med en baslinje som följer slutpunkternas form, och exponera bara den behöriga delmängd som sessionen behöver.
Bättre: utforma verktygen efter hur agenter vill använda dem. Varje verktyg gör en enda väldefinierad sak, tar väldefinierade indata och returnerar väldefinierade utdata.
Några principer:
Ett koncept per verktyg. Ha inte ett manage_customer-verktyg som gör 12 olika saker. Ha search_customers, get_customer, update_customer_email och archive_customer – vart och ett med tydligt fokus.
Rätt detaljnivå. Är verktygen för detaljerade behöver agenten göra många anrop; är de för grova kan den inte utföra precis det som behövs. Sikta på ”operationer som en människa skulle namnge”.
Handlingsverb. search_documents, inte documents. Verktyg ska namnges efter vad de gör.
Skillnad mellan läsning och skrivning. Läsverktyg är säkrare; skrivverktyg har sidoeffekter. Skilj dem åt i namngivningen (list_x respektive create_x) och behandla dem olika (kräv uttrycklig bekräftelse, idempotensnycklar med mera).
Aggregera när det är användbart. Ett get_customer_profile som returnerar kund + senaste beställningar + supportärenden i ett anrop är ofta bättre än tre separata anrop. Agenten får sammanhanget direkt.
Det finns inget universellt korrekt antal verktyg. Exponera bara de verktyg som är relevanta och tillåtna för den aktuella uppgiften, och mät sedan antalet felval när du lägger till eller tar bort verktyg.
Mönster 2: Schemadesign
Varje verktyg har ett indataschema (parametrarna som LLM:en måste ange) och utdata (det verktyget returnerar). Scheman är inte bara till för validering; de är promptutformning.
Indatascheman med Zod:
const searchCustomersSchema = z.object({
query: z.string().describe(
"Sökterm: namn, e-postadress eller företag. Var specifik för att undvika för många träffar."
),
limit: z.number().int().min(1).max(50).default(10).describe(
"Högsta antal resultat som ska returneras. Standardvärde 10, högst 50."
),
filters: z.object({
tier: z.enum(["free", "pro", "enterprise"]).optional().describe(
"Filtrera på en viss kundnivå"
),
status: z.enum(["active", "trial", "churned"]).optional().describe(
"Filtrera på kundstatus"
),
}).optional(),
});
Lägg märke till:
- Varje fält har en
.describe(). Beskrivningen är det som LLM:en läser. - Enum-värden är explicita. Fritext begränsas när det är möjligt.
- Standardvärdena är rimliga.
- Begränsningar (min/max, längd) är uttryckliga.
- Det framgår tydligt vad som är valfritt respektive obligatoriskt.
Beskrivningarna är oerhört viktiga. ”sökterm” hjälper inte; ”Sökterm: namn, e-postadress eller företag. Var specifik för att undvika för många träffar.” ger LLM:en användbar vägledning.
Mönster 3: Utdataform
Utdata är det som LLM:en ser och agerar på. Bra utformade utdata förbättrar LLM:ens beteende dramatiskt.
Strukturerade utdata.
type SearchResult = {
customers: Customer[];
total_matches: number;
truncated: boolean;
next_page_cursor?: string;
};
Med sammanhang.
{
customers: [...],
total_matches: 47,
truncated: true,
next_page_cursor: "abc",
message: "Hittade 47 träffar; visar de första 10. Använd next_page_cursor för att hämta fler."
}
Fältet message ger läsbar vägledning. LLM:er använder den.
Med väl hanterade fel.
{
error: "ambiguous_query",
message: "Söktermen 'john' gav 247 kundträffar. Var mer specifik.",
suggestion: "Försök inkludera ett företagsnamn eller en e-postdomän.",
partial_results: [...] // de tre mest relevanta, valfritt
}
Felet är strukturerat (maskinläsbart), men innehåller också ett meddelande och ett förslag (läsbart för LLM:en). LLM:en kan anpassa sig – antingen be användaren om ett förtydligande eller förfina sökningen.
Lämplig storlek.
Ett verktyg som returnerar en obegränsad mängd poster kan spränga gränserna för kontext, kostnad, latens och dataexponering. Sätt ett tak, paginera med stabila markörer och returnera bara de fält som är tillåtna och nödvändiga för uppgiften. Sammanfattningar är härledda data och behöver proveniens när de exakta posterna spelar roll.
Mönster 4: Felsemantik
Verktyg fallerar. Hur de kommunicerar fel till LLM:en avgör om den återhämtar sig smidigt eller förvärrar felet.
Felkategorier.
type ToolError =
| { type: "validation"; message: string; field?: string }
| { type: "auth"; message: string }
| { type: "not_found"; message: string; suggestion?: string }
| { type: "conflict"; message: string; resolution?: string }
| { type: "rate_limit"; message: string; retry_after_seconds: number }
| { type: "service_unavailable"; message: string; retryable: boolean }
| { type: "internal"; message: string; trace_id: string };
Varje kategori har olika semantik. LLM:en bör reagera olika:
validation: korrigera indata och försök igen.not_found: informera användaren eller prova en annan sökning.conflict: be om ett avgörande.rate_limit: vänta och försök igen.service_unavailable: prova en reservlösning eller meddela användaren.internal: avbryt och visa felet för användaren.
När detta dokumenteras på servern blir LLM:en mer kompetent.
Felformatering.
Returnera fel som strukturerade data med tydliga, åtgärdbara meddelanden:
{
error: {
type: "validation",
message: "E-postadressen har inte ett giltigt format.",
field: "email",
suggestion: "Ange en giltig e-postadress, till exempel 'namn@example.com'."
}
}
Undvik:
{
error: "Ogiltiga indata"
}
Det första gör det möjligt för LLM:en att återhämta sig. Det andra tvingar den att gissa.
Mönster 5: Autentisering och behörighetskontroll
Fjärrservrar för MCP i produktion behöver normalt autentiserade anropare med rätt behörighet, och lokala stdio-servrar ärver fortfarande behörigheterna hos processen som startar dem. Enbart nätverksåtkomst får aldrig ge åtkomst till verktygen.
Autentisering: vem anropar?
Vanliga mönster:
- API-nyckel. Enkelt och vanligt, fungerar mellan tjänster. Utfärda en per konsument och rotera regelbundet.
- OAuth. För system med flera användare där slutanvändare ger agenter behörighet. Mer komplext, men rätt lösning för många användningsfall.
- mTLS. För miljöer med höga säkerhetskrav. Ömsesidiga TLS-certifikat för båda sidor.
Implementeringen beror på transporten. Över HTTP validerar du bärartoken innan MCP-begäran skickas vidare. I v1 kopplar den officiella mellanprogramvaran för bärarautentisering validerad authInfo till hanterarens extra-data; i v2 använder du de aktuella API:erna för resursserver och begärantillstånd. Hitta inte på ett eget applikationsfält context.caller.
server.registerTool("who_am_i", {
description: "Returnera den autentiserade anroparens identitet.",
inputSchema: {},
}, async (_input, extra) => {
if (!extra.authInfo) {
return { isError: true, content: [{ type: "text", text: "Authentication required" }] };
}
const subject = String(extra.authInfo.extra?.sub ?? extra.authInfo.clientId);
return { content: [{ type: "text", text: JSON.stringify({ subject }) }] };
});
Hanteraren förutsätter att HTTP-transporten redan har kört den officiella autentiseringsmellanprogramvaran. Att registrera den på en oautentiserad stdio-server skapar ingen autentisering. Över stdio finns inga HTTP-bärarheaders: åtkomsten styrs normalt av den lokala processgränsen, konfigurationen, filsystemets behörigheter och klienten som startar processen.
Behörighetskontroll: vad får anroparen göra?
När anroparen har autentiserats: vilka verktyg får personen använda och för vilka data?
function authorize(caller: Caller, tool: string, params: any): boolean {
// Anroparnivå: får den här anroparen över huvud taget använda verktyget?
if (!caller.tools.includes(tool)) return false;
// Datanivå: har den här anroparen behörighet till just dessa data?
if (params.tenant_id && params.tenant_id !== caller.tenant_id) return false;
return true;
}
Låt inte LLM:en fatta behörighetsbeslut. Den kan luras. Behörighetskontroll är serverns uppgift; LLM:en ser endast data som den har behörighet att se.
I system med flera kundorganisationer avgränsas varje verktygsanrop till en kundorganisation. Kundorganisationen fastställs av autentiseringen, inte av parametrar som LLM:en anger.
Mönster 6: Idempotens
Idempotens är viktigt för skrivoperationer. LLM:en kan försöka igen eller anropa samma verktyg två gånger i olika sammanhang. Utan idempotens får du dubbletter.
Idempotensnycklar.
Verktyget tar parametern idempotency_key. Servern måste göra anspråk på nyckeln atomiskt i beständig lagring och binda den till den autentiserade principalen, verktygsnamnet och en hash av den normaliserade begäran. En separat läsning följd av en skrivning hamnar i kapplöpning vid samtidighet.
async function createInvoice(params: {
amount: number;
customer_id: string;
idempotency_key: string;
}) {
return database.transaction(async (tx) => {
const claim = await tx.claimIdempotencyKey({
principal_id: currentPrincipal.id,
tool: "create_invoice",
key: params.idempotency_key,
request_hash: hashCanonicalRequest(params),
});
if (claim.request_hash_mismatch) throw new Error("Idempotency key reused for different input");
if (claim.completed_response) return claim.completed_response;
const invoice = await tx.createInvoice(params);
await tx.completeIdempotencyClaim(claim.id, invoice);
return invoice;
});
}
Ge LLM:en en ledtråd om detta i verktygsbeskrivningen:
"Generera ett UUID för varje unik faktura du skapar och skicka det som idempotency_key. Om du behöver försöka utföra operationen igen använder du samma UUID för att undvika dubbletter."
Mönster 7: Strömning
För långa operationer: avgör först om operationen borde vara ett beständigt asynkront jobb. MCP:s förloppsaviseringar är användbara bara när klienten skickar med en förloppstoken och håller anslutningen öppen. Det exakta hanterar-API:et ändrades mellan SDK:ns huvudversioner – utgå från exemplet för din valda SDK-version, inte från den här artikeln.
Se serverexemplet i den valda SDK-versionen för API:erna för förlopp och uppgifter. Skicka aldrig en förloppsavisering med odefinierad token, och använd aldrig en flyktig avisering som enda spår av en operation med konsekvenser.
Använd strömning för:
- Operationer där mellanliggande förlopp är meningsfullt för klienten.
- Stora utdata (så att LLM:en kan börja bearbeta medan utdata fortfarande kommer).
- Operationer med mellanresultat som är värda att visa.
Strömma inte snabba, enkla operationer – det ökar komplexiteten utan att tillföra värde.
Mönster 8: Cachelagring
Många verktygsanrop hämtar samma data upprepade gånger. Cachelagring kan förbättra prestandan dramatiskt och minska belastningen på bakomliggande system.
Lokal cache. Cache i processen (exempelvis LRU) för ofta använd data.
Distribuerad cache. Redis eller motsvarande för en gemensam cache mellan serverinstanser.
Cacheinvalidering. När data ändras avlägsnas relevanta poster. (Det här är den svåra delen.)
TTL-värden. Cachelagrade poster upphör att gälla efter en definierad tid. Anpassa per datatyp – kundprofiler kan cachelagras i timmar, priser i minuter.
För att cachelagring ska hjälpa måste samma behöriga anrop återkomma. Ta med kundorganisation och behörighetsomfång i cachenycklarna, och cachelagra aldrig känsliga svar över anropargränser.
Ett mönster:
async function getCustomerCached(id: string) {
const cached = await cache.get(`customer:${id}`);
if (cached) {
metrics.increment("cache.hit");
return cached;
}
metrics.increment("cache.miss");
const customer = await db.getCustomer(id);
await cache.set(`customer:${id}`, customer, { ttl: 300 });
return customer;
}
Mönster 9: Hastighetsbegränsning
LLM-agenter kan vara förvånansvärt aggressiva – de loopar, försöker igen och sprider anrop. En felande agent kan överbelasta bakomliggande system.
Hastighetsbegränsning per autentiserad principal, klient, verktyg och resurskostnad är nödvändig för en fjärrtjänst. I en v1-verktygshanterare läser du identiteten från validerad extra.authInfo – använd inte ett obefintligt context.caller:
const limiter = new RateLimiter({
windowMs: 60_000,
max: 100 // 100 anrop/minut per anropare
});
server.registerTool("expensive_report", {
description: "Bygg en behörighetskontrollerad rapport.",
inputSchema: { report_id: z.string() },
}, async ({ report_id }, extra) => {
if (!extra.authInfo) return toolError("Authentication required");
const subject = String(extra.authInfo.extra?.sub ?? extra.authInfo.clientId);
if (await limiter.exceeded({ subject, tool: "expensive_report" })) {
return toolError("Rate limit exceeded; retry later");
}
return buildAuthorizedReport(subject, report_id);
});
Utöver globala hastighetsgränser behövs gränser per verktyg – vissa verktyg är dyra och bör begränsas hårt.
Använd strängare gränser eller kräv uttryckliga bekräftelseflöden för operationer med konsekvenser (skapa poster, skicka meddelanden).
Mönster 10: Resurser
MCP har ”resurser” – skrivskyddade datakällor som LLM:en kan bläddra i och hänvisa till. De skiljer sig från verktyg (som anropas aktivt).
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "doc://my-server/handbook",
name: "Personalhandbok",
mimeType: "text/markdown",
description: "Företagets personalhandbok"
},
// ...
]
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const content = await loadResource(request.params.uri);
return { contents: [{ uri: request.params.uri, mimeType: "text/markdown", text: content }] };
});
Resurser är användbara för:
- Referensdokument som LLM:en kan vilja bläddra i.
- Konfigurations- eller kontextdata.
- Uppslagstabeller eller scheman som LLM:en kan behöva.
Resurser läses; verktyg utför åtgärder. Använd rätt koncept för respektive behov.
Mönster 11: Observerbarhet
Mer om mönster på anrops- och spårnivå för LLM-applikationer i allmänhet finns i Observerbarhet för LLM-appar. Instrumentera följande för MCP-servern:
- Varje verktygsanrop: tidsstämpel, autentiserat subjekt eller pseudonymiserad identifierare, verktyg, maskerad sammanfattning av argumenten, klassificering av resultatet, latens och status.
- Mätetal per verktyg: anropsvolym, p50-/p95-latens och felfrekvens.
- Mätetal per anropare: vem anropar och hur ofta.
- Spårningskontext: skicka spår-ID:n från anroparen vidare till bakomliggande anrop.
Pseudokod på applikationsnivå (inte en hanterarkontext i SDK:n):
logger.info("tool_call", {
tool: request.params.name,
caller_id: authenticatedPrincipal.id,
trace_id: currentTraceId,
params: redactPII(request.params.arguments),
duration_ms: duration,
status: "success"
});
Skicka dem till observerbarhetsplattformen.
Mönster 12: Versionshantering
Din MCP-server kommer att utvecklas. Verktyg ändras. Nya verktyg läggs till. Gamla fasas ut.
Serverversionshantering. Konstruktorn McpServer tar ett versionsnummer. Höj det vid ändringar. Klienterna kan upptäcka det.
Verktygsversionering. När ett verktygs signatur ändras på ett inkompatibelt sätt versionerar du det: search_customers_v2. Behåll den gamla versionen under en utfasningsperiod.
Schemautveckling. Valfria fält kan läggas till på ett säkert sätt. Att ta bort fält eller ändra typer är brytande ändringar.
Utfasning. När ett verktyg fasas ut markerar du det i beskrivningen: ”DEPRECATED: use search_customers_v2 instead.”
Versionshantering är nödvändig för MCP-servrar i produktion som används av flera klienter. Servrar som endast används internt kan vara mer flexibla.
Mönster 13: Testning
Hur testar du en MCP-server?
Enhetstester. Varje verktygs logik, med mockade beroenden. Vanlig TypeScript-testning.
Schematester. Scheman validerar som förväntat. Extremfall (saknade fält, fel typer) hanteras korrekt.
Integrationstester. Starta servern, skicka riktiga MCP-begäranden och verifiera svaren. @modelcontextprotocol/sdk innehåller testverktyg.
Heltäckande test med en verklig LLM. Svårast men mest värdefullt. Låt en LLM använda MCP-servern för att utföra realistiska uppgifter. Verifiera att den använder verktygen korrekt. Hitta problem i verktygsbeskrivningarna.
En heltäckande testuppsättning (pseudokod; den exakta klientkopplingen beror på vilken LLM-klient du använder – Anthropics TypeScript-SDK, OpenAI:s eller ett ramverk med MCP-stöd):
// Starta MCP-servern som en underordnad process eller med transport i minnet.
const server = await startTestServer();
// Kör en LLM med MCP-verktygen anslutna. Exakt API beror på klienten.
const result = await runAgent({
mcpServer: server,
systemPrompt: "Du är en kundtjänstagent...",
userMessage: "Hitta kunden Alice och kontrollera hennes öppna ärenden",
});
// Granska de verktygsanrop som servern registrerade under körningen.
expect(server.callLog.map((c) => c.name)).toEqual([
"search_customers",
"list_tickets",
]);
Heltäckande tester upptäcker problem i verktygsbeskrivningar som enhetstester inte kan hitta.
Mönster 14: Driftsättning
Var körs din MCP-server?
Stdio (lokalt). Servern körs som en process och klienten startar den. Passar bäst för skrivbordsappar (Claude Desktop, Cursor) och lokala verktyg.
Streamable HTTP (fjärranslutet). Servern är en nätverkstjänst. Passar bäst för driftade tjänster, delad infrastruktur och åtkomst från flera klienter.
För produktionsservrar:
- Streamable HTTP är den standardiserade kandidaten för fjärrtransport. Verifiera klientstöd, sessionsdesign, behörighetskontroll, skydd av origin och host, proxyer, tidsgränser och skalningsbeteende.
- Driftsätt som vilken webbtjänst som helst: containrar, lastbalansering och automatisk skalning.
- TLS krävs.
- Hälsokontroller för driftsättningsplattformen.
- Kontrollerad avstängning för pågående begäranden.
Mönster 15: Säkerhetsöverväganden
MCP-servrar exponerar funktioner för LLM:er. LLM:er kan manipuleras. Säkerhetskonsekvenser:
Promptinjektion via verktygsindata. En användarbegäran kan innehålla text som försöker lura LLM:en att anropa verktyg på ett skadligt sätt. Försvar:
- Verktygsbeskrivningar som tydligt anger avsedd användning.
- Behörighetskontroll på serversidan (oberoende av parametrar som LLM:en väljer).
- Bekräftelser för åtgärder med konsekvenser.
Dataexfiltrering. Verktyg som returnerar data kan missbrukas – LLM:en kan luras att lämna ut känsliga data på ett olämpligt sätt. Försvar:
- Behörighetskontroller.
- Loggning av vilka data som nås och av vem.
- Mönsterdetektering för ovanliga åtkomstmönster.
Resursuttömning. Verktyg som förbrukar resurser i bakomliggande system kan missbrukas. Försvar:
- Hastighetsbegränsning.
- Resursgränser per verktygsanrop.
- Kretsbrytare när bakomliggande system har försämrats.
Injektion i verktygsutdata. Ett verktygs utdata kan innehålla text som manipulerar LLM:en när den läser den. Försvar:
- Sanera utdata där det är möjligt.
- Var försiktig med verktyg som returnerar användargenererat innehåll.
Det här är verkliga angreppsytor. Behandla MCP-servrar som alla andra produktions-API:er: använd försvar på djupet.
Skiss på hopsättning: inte en komplett HTTP-server
Utdraget nedan visar hur avgränsningen per kundorganisation hör hemma inuti varje verktyg. Det är avsiktligt ofullständigt: db, cache, logger, authenticate, HTTP-routen, mellanprogramvaran för bärarautentisering, livscykelhanteringen och testerna är applikationskod. Klistra inte in det och kalla resultatet driftsatt.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { db, cache, logger, authenticate } from "./infra.js";
const server = new McpServer({
name: "crm-server",
version: "1.0.0",
});
// === Verktyg: search_customers ===
server.registerTool(
"search_customers",
{
description: "Sök efter kunder utifrån namn, e-postadress eller företag.",
inputSchema: {
query: z.string().describe("Namn, e-postadress eller företag"),
limit: z.number().int().min(1).max(50).default(10),
},
},
async ({ query, limit }, extra) => {
const auth = await authenticate(extra);
const cacheKey = `search:${auth.tenant_id}:${query}:${limit}`;
const cached = await cache.get(cacheKey);
if (cached) return cached;
const customers = await db.searchCustomers({
tenant_id: auth.tenant_id,
query,
limit,
});
const result = {
content: [{
type: "text" as const,
text: JSON.stringify({
customers,
total_matches: customers.length,
truncated: customers.length === limit,
message:
customers.length === limit
? `Visar de första ${limit}; det kan finnas fler träffar.`
: `Hittade ${customers.length} kund(er).`,
}),
}],
};
await cache.set(cacheKey, result, { ttl: 60 });
logger.info("search_customers", { tenant: auth.tenant_id, query, results: customers.length });
return result;
}
);
// === Verktyg: get_customer ===
server.registerTool(
"get_customer",
{
description: "Hämta en enskild kund utifrån id.",
inputSchema: { customer_id: z.string() },
},
async ({ customer_id }, extra) => {
const auth = await authenticate(extra);
const customer = await db.getCustomer(auth.tenant_id, customer_id);
if (!customer) {
return {
isError: true,
content: [{
type: "text" as const,
text: `Kunden ${customer_id} hittades inte. Använd search_customers för att söka efter namn eller e-postadress.`,
}],
};
}
return { content: [{ type: "text" as const, text: JSON.stringify({ customer }) }] };
}
);
// === Verktyg: update_customer_email (med idempotens) ===
server.registerTool(
"update_customer_email",
{
description: "Uppdatera en kunds e-postadress; skicka samma idempotency_key vid ett nytt försök.",
inputSchema: {
customer_id: z.string(),
new_email: z.string().email(),
idempotency_key: z
.string()
.describe("UUID för denna uppdatering; skicka samma värde vid ett nytt försök för att undvika dubbletter"),
},
},
async (params, extra) => {
const auth = await authenticate(extra);
// ... idempotenskontroll, validering, uppdatering
return { content: [{ type: "text" as const, text: "ok" }] };
}
);
// ... fler verktyg ...
// Avsiktligt utelämnat: autentiserad Streamable HTTP-route och transportens livscykel.
// Utgå från det officiella exemplet för exakt den valda SDK-versionen.
Det här är en designskiss, inte en körbar slutpunkt. En produktionsimplementation behöver dessutom ett officiellt transportexempel för den valda SDK-versionen, skydd mot host-header- och DNS-rebinding-attacker, OAuth-resursmetadata där det är tillämpligt, TLS vid driftgränsen, behörighetstester, lagring för idempotens, maskering i telemetrin, gränsvärden, kontrollerad avstängning och en testad klientmatris.
Det som skiljer produktionsservrar från demonstrationer
MCP är ett avgränsat protokoll, men att bygga en server av produktionsklass är fortfarande API- och säkerhetsingenjörsarbete. En standardkompatibel tjänst kan minska integrationsarbetet per klient, men bevisen för kompatibilitet gäller bara de klienter, versioner, transporter, autentiseringsvägar och verktyg som ingick i den testade matrisen.
Mönstren som är viktiga är fokuserad verktygsdesign, promptmedvetna scheman, strukturerad felsemantik, robust autentisering, idempotens, observerbarhet och säkerhet. Om du hoppar över någon av dem får du en MCP-server som fallerar i produktion.
Bygg in dem. Testa mot verkliga LLM:er. Iterera på verktygsbeskrivningarna. Resultatet är en tjänst som agenter kan anropa tillförlitligt – utan en egen integration för varje modell.



