Napisz serwer z szablonu
Serwer to aplikacja Spring Boot ze starterem (mcp-platform-starter). Piszesz narzędzia, polityki zasobów i wywołania swojego systemu. Starter weryfikuje token bramki, podaje ci poświadczenie, sprawdza politykę dla każdego zasobu, który narzędzie nazywa, zapisuje audyt wywołania i pilnuje, żeby niebezpieczne błędy nie wychodziły na zewnątrz. Stos: Java 25, Spring Boot 4.1.0, starter 4.0.0, kontrakty 7.0.0.
Zacznij od Zbuduj i zarejestruj pierwszy serwer: tam powstaje projekt, który zmieniasz na tej stronie.
Układ projektu
Dział zatytułowany „Układ projektu”| Moduł | Zawiera |
|---|---|
mcp-core |
Typy domenowe i porty |
mcp-application |
Przypadki użycia |
mcp-integration |
Klient twojego systemu, resolver zasobów, sonda wersji |
mcp-tools |
Narzędzia MCP i ich błędy |
mcp-app |
Start aplikacji, application.yml, polityki, jedyny jar, testy end-to-end |
Zastąp przykładową domenę rekordów w pierwszych trzech modułach zasobami i wywołaniami API swojego systemu. Atrapa backendu z szablonu (StubBackend w mcp-app) pozwala uruchamiać testy bez Dockera i prawdziwego systemu.
Napisz narzędzie
Dział zatytułowany „Napisz narzędzie”Narzędzia rejestruje się beanem ToolCallbackProvider. Sam bean z metodami @Tool nie wystarczy.
@BeanToolCallbackProvider notesTools(NotesTool tool) { return SchemaLimitedToolCallbacks.of(tool);}
@Tool(name = "get_note", description = "Returns one note by its id: id, project, title and status.")@ToolRisk(RiskLevel.READ_ONLY)@ToolTitle("Get note")public NoteView getNote( @ToolParam(description = "Note id as the system issues it") @ArgumentLimits(maxLength = 16, resourceType = "Notes::Note") @ResourceRef(type = "Notes::Note") String noteId) { Note note = ResourceContext.required().fetched("Notes::Note", noteId, Note.class).orElseThrow(); return NoteView.of(note);}Każde narzędzie potrzebuje:
| Element | Reguła |
|---|---|
@ToolTitle |
Niepusty, do 100 znaków |
@ToolRisk |
READ_ONLY, UPDATE lub DESTRUCTIVE. Bez niego albo z nazwą sprzeczną z nim (delete_* jako READ_ONLY) serwer się nie uruchomi |
@ResourceRef |
Na każdym argumencie, który nazywa zasób, z typem zasobu |
@ArgumentLimits |
Ograniczenie na każdym poziomie: maxLength dla tekstu, minimum i maximum dla liczb, maxItems dla list. resourceType bierze wzorzec identyfikatora z resource-types.json |
| Zamknięty schemat | additionalProperties: false. Pisze go SchemaLimitedToolCallbacks. Map, unii i listy list nie da się ograniczyć, zatrzymują start |
Brak argumentu confirmation_code |
Bramka dodaje go do tools/list i usuwa przed wywołaniem |
Poza profilem dev narzędzie z otwartym poziomem schematu zatrzymuje start. W dev dostajesz ostrzeżenie event=tool_schema_open.
Klasy ryzyka
Dział zatytułowany „Klasy ryzyka”@ToolRisk |
Znaczenie | Akcja Cedar | Grupa potrzebna użytkownikowi | Zachowanie bramki |
|---|---|---|---|---|
READ_ONLY |
Niczego nie zmienia | read |
mcp-<system>-read |
Działa po zatwierdzeniu |
UPDATE |
Zmienia dane | update |
mcp-<system>-write |
Działa po zatwierdzeniu; nigdy nie jest ponawiane |
DESTRUCTIVE |
Usuwa lub nie da się cofnąć | delete (lub jawnie admin) |
mcp-<system>-destructive |
Najpierw prosi użytkownika o kod jednorazowy |
Serwer nie prosi o potwierdzenie; tylko prawdziwie deklaruje ryzyko. Nazwa zawierająca delete, drop, remove, purge, truncate, destroy, wipe lub erase nie przejdzie jako READ_ONLY ani UPDATE. Żeby zawęzić narzędzie do dodatkowej grupy, użyj @ToolRisk(value = RiskLevel.READ_ONLY, additionalGroup = "mcp-<system>-ops").
Wywołanie UPDATE może dotrzeć dwa razy, więc wybieraj stan docelowy (set_assignee) zamiast przyrostu, twórz wpisy z kluczem, który możesz sprawdzić, i wysyłaj oczekiwaną wersję, gdy API ją przyjmuje. Gdy system odpowie upstream_unavailable, napisz w komunikacie, że operacja mogła zostać wykonana.
Zasoby i resolver
Dział zatytułowany „Zasoby i resolver”@ResourceRef sprawia, że starter rozwiązuje każdy identyfikator twoim ResourceResolver, z poświadczeniem wywołującego, i ocenia politykę, zanim metoda się wykona. Zasób brakujący, ukryty lub spoza zakresu nigdy nie dociera do twojego kodu; klient dostaje not_found_or_denied we wszystkich trzech przypadkach.
@Componentclass NoteResolver implements ResourceResolver<Note> { public String resourceType() { return "Notes::Note"; }
public Optional<Note> fetch(String id) { return notes.find(id); // pusty dla 404 i 403; wyjątek przy awarii systemu }
public ResolvedResource describe(Note note) { List<String> scopes = List.of(Scopes.id("Notes::Project", note.projectKey())); ResolvedResource project = ResolvedResource.of("Notes::Project", note.projectKey(), Map.of("key", note.projectKey(), "category", note.projectCategory())).withScopes(scopes); return ResolvedResource.of("Notes::Note", note.id(), Map.of("key", note.id())) .withParent(project).withScopes(scopes); }}Identyfikator, rodzic i klasyfikacja pochodzą z systemu, nigdy z argumentu. Zasób bez wymaganej klasyfikacji nie jest budowany, a wywołanie kończy się internal_error. W narzędziu zwracającym listy zasobów elementy są sprawdzane po wywołaniu, a niedozwolone usuwane.
Tekst z systemu (tytuł, komentarz, opis) trafia do wyniku jako UntrustedText, żeby klienty traktowały go jako dane. Wynik musi być rekordem; starter publikuje z niego outputSchema, a składowa, która może być pusta, wymaga @Nullable.
Zadeklaruj zasoby i napisz polityki
Dział zatytułowany „Zadeklaruj zasoby i napisz polityki”Polityki leżą w mcp-app/src/main/resources/mcp/policies/ i trafiają do jara oraz manifestu. Starter odmawia startu bez nich.
| Plik | Zawartość |
|---|---|
schema.cedarschema |
Jedna przestrzeń nazw, własna serwera (Notes): typ encji dla każdego zasobu, każdy z scopes: Set<ResourceScope>, oraz akcje read, update, delete, admin |
resource-types.json |
Jedna deklaracja na typ encji: attributes, parent i parentAttribute, idPattern, opcjonalnie scopePattern, earlyScopeCheck, classification |
*.cedar |
Reguły dostępu, każda z @id unikalnym w serwerze |
Polityka nazywająca Action::"call" lub Tool jest odrzucana: ta decyzja należy do bramki. Pisz tylko reguły dostępu. Platforma sama generuje regułę dla danych restricted i regułę dla konta serwisowego, więc serwer nie może ich pominąć.
Reguła odczytu i reguła zmiany z szablonu:
@id("notes-read")permit ( principal in UserGroup::"mcp-notes-read", action == Notes::Action::"read", resource)when { resource is Notes::Project || resource is Notes::Note };
@id("notes-update-in-scope")permit ( principal in UserGroup::"mcp-notes-write", action == Notes::Action::"update", resource is Notes::Note)when { principal in resource.scopes };principal in resource.scopes jest prawdziwe, gdy jedna z grup użytkownika jest zmapowana na zakres zasobu. Mapowanie robi administrator w portalu; serwer nigdy nie wyprowadza nazwy grupy. Mapowanie zakresów, dozwolonych klientów AI i nadzwyczajne wyjątki nie są konfiguracją serwera: wysyła je bramka.
Zmieniona polityka lub deklaracja zmienia policiesHash w manifeście i przenosi wdrożony serwer w stan pending_approval, dopóki administrator jej nie zatwierdzi. Do tego czasu serwer odrzuca wywołania z policies_not_approved lub policies_differ (dla klienta oba to upstream_unavailable).
Skonfiguruj serwer
Dział zatytułowany „Skonfiguruj serwer”application.yml szablonu to konfiguracja produkcyjna. Te wartości różnią się między środowiskami i pochodzą z deploy/server.env:
| Zmienna | Opis |
|---|---|
MCP_GATEWAY_JWKS_URI |
Zestaw kluczy bramki: https://<platform>/.well-known/mcp-jwks.json |
MCP_CONNECT_URL |
Strona portalu, na której użytkownik łączy konto: https://<platform>/connections?system={system} |
MCP_NOTES_URL |
Adres API twojego systemu, https poza profilem dev |
MCP_ACCEPTED_CREDENTIAL_TYPES |
Domyślnie user-secret. Jeden z user-oauth, user-secret, token-exchange albo samo service-account |
Pierwszy akceptowany typ staje się context.credentialType w każdym żądaniu Cedar. Nigdy nie mieszaj service-account z typami użytkownika: serwer się nie uruchomi. Typ musi być jednym z tych, które broker platformy zwraca dla systemu, zobacz Systemy.
Certyfikat TLS serwera jest generowany raz do MCP_STATE_DIR i używany ponownie, więc trzymaj ten katalog na wolumenie. Bramka przypina jego odcisk, więc nowy certyfikat wymaga nowego zatwierdzenia. Ustaw MCP_TLS_CERT i MCP_TLS_KEY albo MCP_TLS_KEYSTORE i MCP_TLS_STORE_PASSWORD, by użyć certyfikatu z urzędu, któremu bramka ufa.
Przetestuj
Dział zatytułowany „Przetestuj”Testy nie potrzebują Dockera ani platformy. Uruchamia je ./mvnw -B verify.
Polityki testuje się zwykłym JUnit przez PolicyDecisions z mcp-platform-test:
PolicyDecisions policies = PolicyDecisions.fromResources() .scopeMapping(Map.of("mcp-notes-proj-ABC", List.of("Notes::Project/ABC")));
policies.assertAllowed(PolicyDecisions.caller("mcp-notes-write", "mcp-notes-proj-ABC"), "update", note);policies.assertDenied(PolicyDecisions.caller("mcp-notes-write"), "update", note);Dla każdej polityki napisz przypadek dozwolony i odmowny: zakres obejmujący zasób i taki, który go nie obejmuje, dane restricted, konto serwisowe, klient niedozwolony przez bramkę, nadzwyczajny wyjątek. Dla narzędzi TestGateway podpisuje wewnętrzne JWT kluczami testowymi kontraktów, wysyła dane polityk i wywołuje serwer w profilu dev (@ActiveProfiles("dev")). Każde nowe narzędzie potrzebuje też testu negatywnego.
Żeby wywołać narzędzia z klienta MCP bez platformy, uruchom LocalGateway. Po ./mvnw verify:
java -jar ~/.m2/repository/pl/aiwcorpo/mcp/mcp-platform-test/<platform version>/mcp-platform-test-<platform version>-local-gateway.jar \ --server mcp-notes --jwks-port 18445 --policies-dir mcp-app/src/main/resources/mcp/policies \ --scope-mapping .run/scope-mapping.json --env-file local-gateway.env &SPRING_PROFILES_ACTIVE=dev java -jar mcp-app/target/mcp-notes.jar<platform version> to mcp-platform.version z pom.xml. Podłącz klienta do http://127.0.0.1:18500/mcp z Authorization: Bearer <twoj-token>; token jest wysyłany do twojego systemu jako poświadczenie. Lokalny użytkownik ma grupy mcp-notes-read i mcp-notes-write. Nie ma logowania, zatwierdzania narzędzi ani kodu potwierdzenia. Wywołanie dociera do systemu z pełnymi uprawnieniami wysłanego tokenu.
Zbuduj obraz i uruchom
Dział zatytułowany „Zbuduj obraz i uruchom”Katalog deploy/ zawiera compose.yaml, server.env i jednostkę systemd. Zbuduj i uruchom:
./mvnw -B verifycd deploydocker compose builddocker compose up -dJar to mcp-app/target/mcp-notes.jar. Kilka instancji za jednym adresem potrzebuje tego samego Redisa (MCP_REDIS_HOST, żeby token był użyty raz na wszystkich instancjach), tego samego MCP_PAGING_CURSOR_KEY (openssl rand -base64 32) i tego samego certyfikatu. Dla hosta bez Dockera deploy/systemd/ ma jednostkę i plik środowiska.
Typowe problemy:
| Objaw | Przyczyna | Rozwiązanie |
|---|---|---|
| Serwer zatrzymuje się przy starcie: nie można pobrać zestawu kluczy | Host nie rozwiązuje <platform> albo nie ufa jego certyfikatowi |
Sprawdź DNS; dodaj urząd firmowy do magazynu zaufania w deploy/certs/ i ustaw JAVA_TOOL_OPTIONS w server.env |
Portal: tls_handshake_failed lub manifest_unreachable |
Port 8443 jest zamknięty między platformą a serwerem | Otwórz go dla hosta platformy |
Status serwera certificate_changed |
Wolumen stanu odtworzono i wygenerowano nowy certyfikat | Zatwierdź nowy odcisk |
Wywołania kończą się policies_differ |
Nowa lub zmieniona polityka czeka na zatwierdzenie | Poproś administratora o zatwierdzenie polityk |
Wywołania kończą się upstream_unavailable, a w logu jest SSLHandshakeException |
W magazynie zaufania JVM brakuje urzędu twojego systemu | Zaimportuj urząd i odtwórz kontener |
Wydanie
Dział zatytułowany „Wydanie”Wygenerowany serwer ma .github/workflows/ci.yml i release.yml. Tag vX.Y.Z zgodny z wersją projektu uruchamia wydanie: testy, budowę manifestu i SBOM, wypchnięcie obrazu do ghcr.io/<owner>/mcp-server-notes oraz dołączenie manifestu i SBOM do GitHub Release. Zadania wypychające i publikujące wymagają zatwierdzenia w środowisku GitHub release.
-
Ustaw wersję bez
-SNAPSHOTw plikach pom, wdeploy/compose.yaml(domyślna wartośćMCP_NOTES_VERSION) i wCHANGELOG.md. -
Uruchom
./mvnw -B verify. -
Zatwierdź zmiany, otaguj i wypchnij tylko ten tag:
Okno terminala git tag v1.0.0git push origin v1.0.0 -
Zatwierdź zadania w środowisku
release.
Operator pobiera nowy obraz i uruchamia docker compose up -d. Wolumen stanu zachowuje certyfikat, więc połączenie pozostaje zatwierdzone. Wersja z innymi narzędziami lub politykami czeka w portalu na zatwierdzenie administratora, zobacz Serwery.
