MCP nullist: ehita TypeScriptis tootmiskõlblik server
Ekspert14 min lugemistAI ettevõttes

MCP nullist: ehita TypeScriptis tootmiskõlblik server

Tootmiskõlbliku Model Context Protocoli serveri ehitamine nõuab enamat kui paari tööriista kokkuühendamist. Skeemikujunduse, autentimise, vigade käsitlemise, voogedastuse ja seire mustrid ning need tootmisreaalsused, mis muudavad MCP serverid skaalal tegelikult kasulikuks.

Mida oskad pärast teha

Tootmiskõlblik MCP-server on väike ja sihipärane teenus, millel on hästi kavandatud tööriistad, hoolikad skeemid, töökindel veakäsitlus, korralik autentimine ja sisseehitatud seire. Protokoll ise on lihtne; põhiline inseneritöö kulub serveri muutmisele tegelikes AI-töövoogudes päriselt kasulikuks.

Salvestatakse ainult selles brauseris.
Selles artiklis

2026. aasta keskpaigaks on MCP (Model Context Protocol) tegelik standard LLM-ide ühendamiseks tööriistadega. Anthropic tutvustas seda; OpenAI, Google ja laiem ökosüsteem on selle omaks võtnud. Cursor, Claude Desktop, ChatGPT, kohandatud agendid — kõik räägivad MCP-d.

Kui sa tahad, et LLM agendid sinu teenusega suhtleksid, siis MCP server on see viis. Ja kui sa oled ühe-kaks ehitanud, näed, et protokoll ise on väike. Huvitav insenertöö läheb kõigesse, mis selle ümber on: skeemikujundus, vigade käsitlemine, autentimine, voogedastus, jõudlus, seire.

See artikkel on süvasukeldumine tootmiskõlbliku MCP serveri ehitamisse TypeScriptis. Käsitleme mustreid, mis päris agentide kasutuses vastu peavad — mitte ainult protokolli mehaanikat.

Mis MCP lühidalt on

MCP on klient-server protokoll, kus:

  • Serverid pakuvad tööriistu, ressursse ja kutsesid (prompts).
  • Kliendid on tüüpiliselt LLM agendid, kes neid tarbivad.

Protokoll kasutab JSON-RPC 2.0 (ametlik spetsifikatsioon on lühike ja väärt korra läbi lugeda). Transpordid on stdio lokaalsete protsesside jaoks ja Streamable HTTP kaugserverite jaoks — vanem HTTP+SSE transport kuulutati spetsifikatsiooni 2025-03-26 revisjonis aegunuks, nii et iga sellele ehitatud õpetus on ajalugu. Autentimine ja turvalisus on protokolli mured; peamised teostused toetavad OAuthi, API võtmeid jms.

Serveri ülesanne: pakkuda LLM-idele kasulikke võimekusi viisil, mida nad oskavad avastada ja kasutada.

Põhistruktuur

Ametliku @modelcontextprotocol/sdk paketi abil näeb minimaalne server kõrgtaseme McpServer API-ga välja nii:

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: "Tagasta esitatud tekst muutmata kujul.",
    inputSchema: { text: z.string() },
  },
  async ({ text }) => ({
    content: [{ type: "text", text }],
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);

(Sama SDK paljastab ka madalama taseme Server klassi koos setRequestHandler-iga ListToolsRequestSchema / CallToolRequestSchema jaoks, kui tahad päringukäsitlejate üle täielikku kontrolli — aga enamiku serverite jaoks on McpServer.registerTool lühem ja sellega keerulisem vigu teha.)

See on luustik. Töö läheb sinna, mida sa tööriistakäsitlejatesse paned — ja kuidas.

Muster 1: Tööriistade kujunduse filosoofia

Esimene otsus: milliseid tööriistu sa paljastad ja millise granulaarsusega?

Tavaline läbikukkumine: oma alustkihi API paljastamine tööriistadena. Kui sul on 200 REST-i lõpp-punkti, on 200 tööriista paljastamine katastroof. Liiga paljude tööriistadega mudelid esinevad halvemini; tööriistade kirjeldused muutuvad kohmakaks; protokoll muutub labürindiks.

Parem: kujunda tööriistu nii, nagu agendid neid kasutada tahaksid. Iga tööriist teeb üht hästi defineeritud asja, võtab hästi defineeritud sisendid, tagastab hästi defineeritud väljundid.

Mõned põhimõtted:

Üks mõiste tööriista kohta. Ära tee manage_customer tööriista, mis teeb 12 erinevat asja. Tee search_customers, get_customer, update_customer_email, archive_customer — iga oma fookusega.

Õige granulaarsus. Liiga peenelt — agent vajab palju kõnesid; liiga jämedalt — ei saa täpselt seda, mida vaja. Eesmärk on “operatsioonid, millele inimene paneks nime.”

Tegevussõnad. search_documents, mitte documents. Tööriistad peaks olema nimetatud selle järgi, mida nad teevad.

Lugemis-vs-kirjutamis eristus. Lugemistööriistad on turvalisemad; kirjutamistööriistadel on kõrvalmõjud. Erista neid nime poolest (list_x vs create_x) ja kohtle erinevalt (nõua selget kinnitust, idempotentsuse võtmeid jne).

Koonda, kui see on kasulik. get_customer_profile, mis tagastab kliendi, viimased tellimused ja tugipiletid ühe kutsega, on sageli parem kui kolm eraldi kutset. Agent saab konteksti korraga.

Näiteks kliendituge paljastava serveri jaoks võib mõistlik tööriistakomplekt olla 8–15 tööriista. Rohkem on tavaliselt liiga palju.

Muster 2: Skeemikujundus

Igal tööriistal on sisendiskeem (parameetrid, mida LLM peab andma) ja väljund (mida sinu tööriist tagastab). Skeemid pole ainult valideerimiseks, vaid täidavad ka osa promptide kavandamise ülesandest.

Zodi kasutamine sisendiskeemide jaoks:

const searchCustomersSchema = z.object({
  query: z.string().describe(
    "Otsingusõna: nimi, e-posti aadress või ettevõte. Liiga paljude vastete vältimiseks ole täpne."
  ),
  limit: z.number().int().min(1).max(50).default(10).describe(
    "Tagastatavate tulemuste ülempiir. Vaikimisi 10, kõige rohkem 50."
  ),
  filters: z.object({
    tier: z.enum(["free", "pro", "enterprise"]).optional().describe(
      "Filtreeri kindla klienditaseme järgi"
    ),
    status: z.enum(["active", "trial", "churned"]).optional().describe(
      "Filtreeri kliendi oleku järgi"
    ),
  }).optional(),
});

Pane tähele:

  • Igal väljal on .describe(). Kirjeldus on see, mida LLM loeb.
  • Loetelud (enums) on selgesõnalised. Vabavormi stringid on võimaluse korral piiratud.
  • Vaikeväärtused on mõistlikud.
  • Piirangud (min/max, pikkus) on selged.
  • Valikuline vs kohustuslik on selge.

Kirjeldused on väga olulised. „Otsingusõna“ ei anna piisavalt juhiseid; „Otsingusõna: nimi, e-posti aadress või ettevõte. Liiga paljude vastete vältimiseks ole täpne“ on LLM-ile kasulik juhis.

Muster 3: Väljundi kuju

Väljund on see, mida LLM näeb ja millele ta reageerib. Hea väljundikujundus parandab dramaatiliselt LLM-i käitumist.

Struktureeritud väljundid.

type SearchResult = {
  customers: Customer[];
  total_matches: number;
  truncated: boolean;
  next_page_cursor?: string;
};

Kontekstiga.

{
  customers: [...],
  total_matches: 47,
  truncated: true,
  next_page_cursor: "abc",
  message: "Leidsin 47 vastet; kuvan esimesed 10. Järgmiste vastete jaoks kasuta välja next_page_cursor."
}

message väli on inimloetav juhis. LLM-id kasutavad seda.

Vigade käsitlemisega armulikult.

{
  error: "ambiguous_query",
  message: "Otsingusõna 'john' vastas 247 kliendile. Täpsusta päringut.",
  suggestion: "Lisa näiteks ettevõtte nimi või e-posti domeen.",
  partial_results: [...]  // kolm kõige asjakohasemat vastet, valikuline
}

Viga on struktureeritud (masinloetav), aga sisaldab ka teadet ja soovitust (LLM-loetavad). LLM saab kohaneda — kas küsida kasutajalt täpsustust või päringut viimistleda.

Sobiva suurusega.

Tööriist, mis tagastab 10 000 kirjet, on kasutuskõlbmatu. LLM-i konteksti see ei mahu; isegi kui mahuks, ei kasuta LLM seda hästi. Lehekülgenda alati, kärbi või tee kokkuvõte. Tagasta piisavalt, et LLM saaks otsuse teha, mitte kogu olemasolev.

Muster 4: Vigade semantika

Tööriistad kukuvad läbi. See, kuidas nad LLM-ile ebaõnnestumisest teada annavad, määrab, kas LLM taastub graatsiliselt või kuhjab vigu peale.

Vigade kategooriad.

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 };

Igal kategoorial on erinev semantika. LLM peaks reageerima erinevalt:

  • validation: paranda sisend ja proovi uuesti.
  • not_found: ütle kasutajale, või proovi teist otsingut.
  • conflict: küsi lahendust.
  • rate_limit: oota ja proovi uuesti.
  • service_unavailable: proovi tagavaravarianti või teata kasutajale.
  • internal: anna alla, anna kasutajale teada.

Nende dokumenteerimine sinu serveris teeb LLM-i võimekamaks.

Vigade vormindus.

Tagasta vead struktureeritud andmetena, selgete ja teostatavate teadetega:

{
  error: {
    type: "validation",
    message: "E-posti aadress pole sobivas vormingus.",
    field: "email",
    suggestion: "Esita sobivas vormingus aadress, näiteks 'nimi@example.com'."
  }
}

Väldi:

{
  error: "Vigane sisend"
}

Esimene laseb LLM-il taastuda. Teine jätab ta aimama.

Muster 5: Autentimine ja autoriseerimine

Tootmis-MCP serverid vajavad autentimist. Kes iganes serverini ulatub, saab tööriistu kasutada. See on peaaegu alati probleem.

Autentimine: kes helistab?

Tavalised mustrid:

  • API võti. Lihtne, levinud, töötab teenus-teenuselt suhtluseks. Anna iga tarbija kohta; tee perioodiliselt rotatsiooni.
  • OAuth. Mitmekasutaja-süsteemidele, kus lõppkasutajad volitavad agente. Keerukam, aga paljudele kasutusjuhtudele õige vastus.
  • mTLS. Kõrge turvalisusega keskkondadele. Vastastikused TLS sertifikaadid mõlemale poolele.

Teostus oleneb transpordist. HTTP kaudu kontrollid päringu autentimist enne, kui see üldse MCP käsitlejani jõuab (sinu Expressi/Hono/Fastify vahevaras) ja paned helistaja päringule:

// Expressi-laadne vahevara MCP HTTP-lõpp-punkti ees.
app.use("/mcp", async (req, res, next) => {
  const apiKey = req.header("x-api-key");
  const caller = await authenticate(apiKey);
  if (!caller) return res.status(401).send("Autoriseerimata");
  (req as any).caller = caller;
  next();
});

Seejärel iga tööriistakäsitleja sees võta helistaja päringu-tasandi kontekstist (extra), mitte toorest päisest. Stdio kaudu ei ole HTTP päiseid; autentimine tuleb tavaliselt protsessi env-ist / konfifailidest.

Autoriseerimine: mida nad teha tohivad?

Kui kord autenditud, milliseid tööriistu helistaja kasutada saab ja milliste andmete peal?

function authorize(caller: Caller, tool: string, params: any): boolean {
  // Kutsuja tase: kas see kutsuja tohib tööriista üldse kasutada?
  if (!caller.tools.includes(tool)) return false;

  // Andmete tase: kas kutsujal on õigus neid konkreetseid andmeid kasutada?
  if (params.tenant_id && params.tenant_id !== caller.tenant_id) return false;

  return true;
}

Ära lase LLM-il autoriseerimisotsuseid teha. LLM-i võib ära petta. Autoriseerimine on serveri töö; LLM näeb ainult andmeid, mida ta on volitatud nägema.

Mitme-rentniku-süsteemides: iga tööriistakõne on rentnikule ulatatud. Rentnik määratakse autentimise, mitte LLM-i antud parameetrite järgi.

Muster 6: Idempotentsus

Kirjutamisoperatsioonide puhul on idempotentsus oluline. LLM võib uuesti proovida; ta võib sama tööriista kahes erinevas kontekstis kutsuda. Ilma idempotentsuseta saad duplikaate.

Idempotentsuse võtmed.

Tööriist võtab idempotency_key parameetri. Server kontrollib: kas oleme seda võtit varem näinud? Kui jah, tagasta vahemällu pandud tulemus. Kui ei, käivita ja pane vahemällu.

async function createInvoice(params: {
  amount: number;
  customer_id: string;
  idempotency_key: string;
}) {
  const cached = await idempotencyStore.get(params.idempotency_key);
  if (cached) return cached;

  const invoice = await actuallyCreateInvoice(params);
  await idempotencyStore.set(params.idempotency_key, invoice, { ttl: 86400 });
  return invoice;
}

LLM-i jaoks vihja sellele tööriista kirjelduses:

„Iga unikaalse arve loomisel genereeri UUID ja edasta see idempotency_key-na. Kui pead operatsiooni uuesti proovima, kasuta sama UUID-d, et vältida duplikaatide loomist.“

Muster 7: Voogedastus

Tööriistadele, mis toodavad suuri väljundeid või võtavad aega, on väljundi voogedastus parem UX. MCP toetab edenemisteateid tööriistakäsitleja seest läbi päringu-tasandi extra argumendi:

server.registerTool(
  "long_running_task",
  { description: "...", inputSchema: { ... } },
  async (input, extra) => {
    await extra.sendNotification({
      method: "notifications/progress",
      params: { progressToken: extra._meta?.progressToken, progress: 0, message: "Starting..." },
    });

    for (const step of steps) {
      await doStep(step);
      await extra.sendNotification({
        method: "notifications/progress",
        params: {
          progressToken: extra._meta?.progressToken,
          progress: step.index / steps.length,
          message: step.name,
        },
      });
    }

    return { content: [{ type: "text", text: JSON.stringify({ result: finalResult }) }] };
  }
);

Kasuta voogedastust:

  • Pikkadele operatsioonidele (>5 sekundit).
  • Suurtele väljunditele (et LLM saaks töötlemist alustada, kui väljund veel tuleb).
  • Operatsioonidele, kus vahepealsed tulemused on näitamist väärt.

Ära vooga edasta kiirete, lihtsate operatsioonide korral — lisab keerukust ilma kasuta.

Muster 8: Vahemällu salvestamine

Paljud tööriistakõned tabavad samu andmeid korduvalt. Vahemällu salvestamine võib dramaatiliselt parandada jõudlust ja vähendada taustsüsteemi koormust.

Lokaalne vahemälu. Protsessisisene vahemälu (nt LRU) tuliste andmete jaoks.

Hajutatud vahemälu. Redis või sarnane jagatud vahemälu jaoks üle serveri-instantside.

Vahemälu invalideerimine. Kui andmed muutuvad, vabasta asjakohased kirjed. (See on raske osa.)

TTL-id. Vahemällu pandud kirjed aeguvad pärast määratud aega. Sea iga andmetüübi jaoks — kliendi profiilid võivad vahemälus olla tunde; hinnastik võib olla minuteid.

Selleks, et vahemälust kasu oleks, peavad samad kõned korduma. Paljudel MCP serveritel on see olemas — agendid viitavad sageli samadele olemitele korduvalt seansi jooksul.

Muster:

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;
}

Muster 9: Päringusageduse piiramine

LLM agendid võivad olla üllatavalt agressiivsed — silmustes, korduvalt proovides, harust harusse minnes. Pahasti käituv agent võib sinu taustsüsteemi DoS-iga maha lüüa.

Päringusageduse piiramine helistaja kohta on hädavajalik:

const limiter = new RateLimiter({
  windowMs: 60_000,
  max: 100  // 100 calls/minute per caller
});

server.setRequestHandler(CallToolRequestSchema, async (request, context) => {
  if (await limiter.exceeded(context.caller.id)) {
    return errorResponse("rate_limit", "Too many requests");
  }
  // ...
});

Lisaks globaalsetele piiridele on tööriistapõhised piirid olulised — mõned tööriistad on kallid ja neid tuleks tihedalt piirata.

Tagajärgedega operatsioonide (kirjete loomine, teadete saatmine) korral kasuta rangemaid piiranguid või nõua selget kinnitusvoogu.

Muster 10: Ressursid

MCP-l on “ressursid” — kirjutuskaitstud andmeallikad, mida LLM saab sirvida ja viidata. Erineb tööriistadest (mida kutsutakse aktiivselt).

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri: "doc://my-server/handbook",
      name: "Employee Handbook",
      mimeType: "text/markdown",
      description: "Company employee handbook"
    },
    // ...
  ]
}));

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const content = await loadResource(request.params.uri);
  return { contents: [{ uri: request.params.uri, mimeType: "text/markdown", text: content }] };
});

Ressursid on kasulikud:

  • Viitedokumendid, mida LLM võib tahta sirvida.
  • Konfiguratsioon või konteksti-andmed.
  • Otsingutabelid või skeemid, mida LLM võib vajada.

Ressurssi loetakse; tööriistad on tegevused. Kasuta mõlema jaoks õiget mõistet.

Muster 11: Seire

Samad mustrid nagu mujal tootmis-AI-s. Sinu MCP serveri jaoks instrumenteeri:

  • Iga tööriistakõne: ajatempel, helistaja, tööriist, parameetrid, tulemus, latentsus, staatus.
  • Tööriistapõhised mõõdikud: kõnede maht, p50/p95 latentsus, vigade määr.
  • Helistajapõhised mõõdikud: kes helistab, kui sageli.
  • Jälje (trace) kontekst: edasta jälje ID-d helistajalt taustsüsteemi kõnedeni.

Struktureeritud logid:

logger.info("tool_call", {
  tool: request.params.name,
  caller_id: context.caller.id,
  trace_id: context.trace_id,
  params: redactPII(request.params.arguments),
  duration_ms: duration,
  status: "success"
});

Suuna seireplatvormi.

Muster 12: Versioonimine

Sinu MCP server areneb. Tööriistad muutuvad. Uusi tööriistu lisandub. Vanu tööriistu kaotatakse.

Serveri versioonimine. Server konstruktor võtab versiooni. Tõsta seda muudatustel. Kliendid saavad seda tuvastada.

Tööriista versioonimine. Kui tööriista signatuur mittesobivalt muutub, versioneeri seda: search_customers_v2. Hoia vana versiooni saadaval kasutuselt kõrvaldamise perioodi jooksul.

Skeemi areng. Lisa valikulisi välju turvaliselt. Väljade eemaldamine või tüüpide muutmine on mittesobiv muudatus.

Kasutuselt kõrvaldamine. Tööriista kõrvaldades märgi see kirjelduses: “DEPRECATED: use search_customers_v2 instead.”

Mitme kliendi poolt kasutatavate tootmis-MCP serverite jaoks on versioonimine hädavajalik. Sisemiselt kasutatavad serverid võivad olla paindlikumad.

Muster 13: Testimine

Kuidas MCP serverit testida?

Ühiktestid. Iga tööriista loogika, simuleeritud sõltuvustega. Tavaline TypeScripti testimine.

Skeemi testid. Skeemid valideerivad nagu oodatud. Erijuhud (puuduvad väljad, valed tüübid) on käsitletud õigesti.

Integratsioonitestid. Käivita server, saada päris MCP päringud, kontrolli vastuseid. @modelcontextprotocol/sdk sisaldab testimisabivahendeid.

Otsast-otsani päris LLM-iga. Raskeim, aga väärtuslikem. Lase LLM-il sinu MCP serverit kasutada realistlike ülesannete täitmiseks. Kontrolli, et LLM kasutab tööriistu õigesti. Leia tööriistakirjelduste probleeme.

Otsast-otsani testide ülesseadmine (pseudokood; täpne kliendiühendus sõltub kasutatavast LLM kliendist — Anthropici TypeScripti SDK, OpenAI oma või MCP-d toetav raamistik):

// Käivita MCP-server alamprotsessina või mälusisese transpordiga.
const server = await startTestServer();

// Käivita LLM koos MCP-tööriistadega. Täpne API sõltub kliendist.
const result = await runAgent({
  mcpServer: server,
  systemPrompt: "Oled klienditoe agent...",
  userMessage: "Leia klient Alice ja kontrolli tema avatud pileteid",
});

// Kontrolli tööriistakutseid, mille server käivituse ajal talletas.
expect(server.callLog.map((c) => c.name)).toEqual([
  "search_customers",
  "list_tickets",
]);

Otsast-otsani testid püüavad kinni tööriistakirjelduste probleemid, mida ühiktestid ei suuda.

Muster 14: Juurutamine

Kus sinu MCP server elab?

Stdio (lokaalne). Server jookseb protsessina; klient käivitab selle. Parim töölauarakendustele (Claude Desktop, Cursor) ja kohalikele tööriistadele.

HTTP/SSE (kaug). Server on võrguteenus. Parim majutatud teenustele, jagatud infrastruktuurile, mitmest kliendist juurdepääsule.

Tootmisserverite jaoks:

  • Streamable HTTP on tavaliselt valik.
  • Juuruta nagu iga veebiteenust: konteinerid, koormuse jaotus, autoskaleerimine.
  • TLS nõutav.
  • Tervisekontrollid juurutusplatvormile.
  • Graatsiline väljalülitus jooksvate päringute jaoks.

Muster 15: Turvakaalutlused

MCP serverid paljastavad võimekused LLM-idele. LLM-e saab manipuleerida. Turvalisuse mõjud:

Promptide süstimine läbi tööriistasisendite. Kasutaja päring võib sisaldada teksti, mis üritab LLM-i petta kahjulikult tööriistu kasutama. Kaitseks:

  • Tööriistade kirjeldused selged oodatud kasutuse osas.
  • Autoriseerimine serveri poolel (sõltumatult LLM-i otsustatud parameetritest).
  • Kinnitused tagajärgedega tegevustele.

Andmete väljaviimine. Tööriistu, mis tagastavad andmeid, saab kuritarvitada — LLM-i võib ära petta, et see tagastaks tundlikke andmeid sobimatult. Kaitseks:

  • Autoriseerimiskontrollid.
  • Logimine, milliseid andmeid kes pääseb.
  • Mustrite tuvastamine ebatavaliste juurdepääsumustrite jaoks.

Ressursside ammendamine. Tööriistu, mis tarbivad taustsüsteemi ressursse, saab kuritarvitada. Kaitseks:

  • Päringusageduse piiramine.
  • Ressursipiirid tööriistakõne kohta.
  • Kaitselülitid (circuit breakers), kui taustsüsteem on halvenenud.

Süstimine tööriista väljunditesse. Tööriista väljund võib sisaldada teksti, mis LLM-i poolt lugedes seda manipuleerib. Kaitseks:

  • Sanitiseeri väljundeid võimaluse korral.
  • Ole ettevaatlik tööriistadega, mis tagastavad kasutaja loodud sisu.

Need on päris ründepinnad. Kohtle MCP servereid nagu iga tootmis-API-d: kaitse sügavuti.

Täielik näide: väike-aga-päris MCP server

Et see kokku panna, server, mis paljastab väikese CRM-i:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import { db, cache, logger, authenticate } from "./infra.js";

const server = new McpServer({
  name: "crm-server",
  version: "1.0.0",
});

// === Tool: search_customers ===

server.registerTool(
  "search_customers",
  {
    description: "Search customers by name, email, or company.",
    inputSchema: {
      query: z.string().describe("Name, email, or company"),
      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
              ? `Kuvan esimesed ${limit} vastet; vasteid võib olla rohkem.`
              : `Leidsin ${customers.length} klienti.`,
        }),
      }],
    };

    await cache.set(cacheKey, result, { ttl: 60 });
    logger.info("search_customers", { tenant: auth.tenant_id, query, results: customers.length });
    return result;
  }
);

// === Tool: get_customer ===

server.registerTool(
  "get_customer",
  {
    description: "Leia üks klient ID järgi.",
    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: `Klienti ${customer_id} ei leitud. Nime või e-posti aadressi järgi otsimiseks kasuta tööriista search_customers.`,
        }],
      };
    }
    return { content: [{ type: "text" as const, text: JSON.stringify({ customer }) }] };
  }
);

// === Tööriist: update_customer_email (idempotentsusega) ===

server.registerTool(
  "update_customer_email",
  {
    description: "Uuenda kliendi e-posti aadressi; korduskatsel kasuta sama idempotency_key väärtust.",
    inputSchema: {
      customer_id: z.string(),
      new_email: z.string().email(),
      idempotency_key: z
        .string()
        .describe("Selle uuenduse UUID; duplikaatide vältimiseks kasuta korduskatsel sama väärtust"),
    },
  },
  async (params, extra) => {
    const auth = await authenticate(extra);
    // ... idempotentsuse kontroll, valideerimine ja uuendamine
    return { content: [{ type: "text" as const, text: "ok" }] };
  }
);

// ... muud tööriistad ...

// Ühenda kaugtransport (voogedastatav HTTP) valitud pordil enda valitud HTTP-serveri kaudu.
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() });
await server.connect(transport);

See on lähtestruktuur. Lisa seire, päringusageduse piiramine, rohkem tööriistu, hoolikamad skeemid — aga raamid on käes.

Mis eristab tootmisserverit demost

MCP on väike protokoll; tootmiskõlbliku serveri ehitamine on päris insenertöö. Tasu: sinu teenus muutub kasutatavaks iga LLM agendi poolt, standardiseeritud integratsiooniga, mis ei nõua mudelipõhist sidustamist.

Olulised mustrid on sihipärane tööriistakujundus, prompte arvestavad skeemid, struktureeritud veasemantika, töökindel autentimine, idempotentsus, seire ja turvalisus. Neist ühegi vahelejätmine võib luua MCP-serveri, mis tootmises läbi kukub.

Ehita need sisse. Testi päris LLM-idega. Täiusta tööriistakirjeldusi. Tulemuseks on teenus, mida LLM saab kasutada sama sujuvalt kui inimene ja mis tuleb toime AI-agentide kasvava hulga ning keerukusega.

Järgmisena loe

Jätka sama õpiteekonda järgmiste praktiliste artiklitega.