Conformance¶
The conformance check reads a repository's files and says whether it is built the way the standard
asks: the same global.json, .editorconfig and workflows as every other Snail repository, tests, benchmarks, a
written-down public API, a server's manifest. It runs in every CI and release, so a repository that drifts from the
standard fails its build the day it drifts rather than at its next release. This page lists every check with what it
requires and how to make it hold, how to run it, and the errors of the reference generator.
Running it¶
In a repository on Snail.Sdk, on any of its projects:
dotnet msbuild src/<Project> -t:SnailConformance
dotnet msbuild src/<Project> -t:SnailConformance -p:SnailConformanceRelease=true # with the release checks
The target runs the check on the folder of the project's global.json. Every unmet required check becomes an MSBuild
error carrying its id, error S3LC0011: shared workflows: <what was seen>, and every unmet advice a warning, so a build
log, an editor and CI show them like any other diagnostic. lib-ci.yml and mcp-ci.yml run it as it is;
lib-publish.yml and mcp-publish.yml run it with the release checks on.
The same check is a program, Snail.Sdk.Conformance.dll in the tools/conformance folder of the Snail.Sdk package
(from source: dotnet run --project src/Conformance --):
| Command | Does | Exit code |
|---|---|---|
Snail.Sdk.Conformance <repository> [--release] |
checks one repository: every check, the unmet first, then each unmet one again as an MSBuild line | 0 when every required check holds, 1 when one fails, 2 on a wrong command line |
Snail.Sdk.Conformance --fleet <folder>... [--report <file.md>] [--release] |
checks every repository in the folders and writes a Markdown table, a column per check, then what each unmet check saw | always 0; stderr says how many conform |
Snail.Sdk.Conformance --rules |
prints the table of every check below | 0 |
--release adds the release checks (S3LC0026). A repository of a fleet is a folder holding global.json beside
.git — a folder, or a file as in a worktree or a submodule; Snail.Sdk itself is left out, being the standard.
A repository is a server when it keeps .mcp/server.json or its name starts with Snail.MCP.; otherwise an
application when a project of its own, not a test, builds on Microsoft.NET.Sdk.Web; and a library otherwise.
An application publishes no package and no API reference: it is released as its image. Ids are stable: a check that is
dropped keeps its number retired, never reused.
The checks¶
The table is the output of --rules of this release. "Fails the build" no means advice: reported, never failing.
| Id | Check | Asked of | Fails the build | What holds |
|---|---|---|---|---|
| S3LC0001 | one .slnx | all | yes | one .slnx at the root, no .sln |
| S3LC0002 | global.json | all | yes | the sdk section equal in value to Snail.Sdk's own (property order and formatting aside) |
| S3LC0003 | central versions | all | yes | Directory.Packages.props with ManagePackageVersionsCentrally, no Version= on a PackageReference in a project |
| S3LC0004 | .editorconfig | all | yes | the shared .editorconfig, line for line (CRLF or LF alike) |
| S3LC0005 | LICENSE | all | yes | a LICENSE file at the root |
| S3LC0006 | nuget.config | all | yes | nuget.config with a packageSourceMapping |
| S3LC0007 | on Snail.Sdk | all | yes | every project names a Snail.Sdk level, as its Sdk attribute or an <Sdk> element |
| S3LC0008 | tests | all | yes | a test project: *.Tests, or SnailProjectKind Tests |
| S3LC0009 | benchmarks | all | yes | a benchmark project: *.Benchmarks, or SnailProjectKind Benchmarks (a Tool does not count) |
| S3LC0010 | benchmark baseline | all | yes | baseline/*-report.csv beside a benchmark project — the reports perf.yml compares with |
| S3LC0011 | shared workflows | all | yes | ci.yml, docs.yml, perf.yml call the shared workflows of the right level; publish.yml has a job calling the shared lib-publish.yml/mcp-publish.yml without id-token: write and a job using the nuget-push action with it; a library may release with a workflow of its own (nuget push) in place of publish.yml. An application keeps ci.yml calling app-ci.yml, image.yml calling mcp-image.yml and perf.yml, and no package or site |
| S3LC0012 | dependabot | all | yes | .github/dependabot.yml with version: 2 and the github-actions and nuget ecosystems |
| S3LC0013 | README | all | yes | README.md whose first non-blank line is an H1 (#) |
| S3LC0014 | reference generated | all | yes | docs/reference committed, or a server's tool pages where SnailReferenceFolder writes them; an application has no API to reference |
| S3LC0015 | site theme | all | yes | no docs/theme, docs/requirements.txt or theme: in docs/mkdocs.yml — the site wears the SDK's |
| S3LC0016 | native warnings | native | yes | a native repository (a library with a *.NativeProbe, a server with SnailMcpNative not false) silences no trim or AOT warning: no IL code in a NoWarn, and no IlcTreatWarningsAsErrors false unless a workflow of the repository names the forgiven warnings in forgiven-native-warnings (the shared CI then fails on any other) |
| S3LC0017 | migration debt | all | no | no NoWarn in any project or props file — advice |
| S3LC0018 | public API written down | library | yes | PublicAPI.Shipped.txt beside every packed project (not a kind, not IsPackable false) |
| S3LC0019 | package baseline | library | yes | SnailPackageBaseline in Directory.Build.props once released: a PublicAPI.Shipped.txt lists an API, or a v* tag of an earlier release exists — the tag of the release being made does not count |
| S3LC0020 | native probe | library | yes | a *.NativeProbe project, or SnailNotNativeBecause saying why not |
| S3LC0021 | server.json | server | yes | .mcp/server.json with the current registry $schema, a name ending in the short name, a description of at most 100 characters, a version other than 0.0.0-dev, packages naming the repository's package, transports matching SnailMcpTransport (Both: stdio and streamable-http), every variable SNAIL_MCP_<NAME>_ |
| S3LC0022 | tool families | server | yes | docs/tools.json |
| S3LC0023 | contract and guard rules | server | yes | a test through Snail.Sdk.Mcp.Testing and one with ToolGuards |
| S3LC0024 | data directory | server | no | its folders in the home under the s3l name, through ServerFolders — advice |
| S3LC0025 | container image | server, application | yes | an application, or a server with SnailMcpContainer true: a Dockerfile, and a workflow calling the shared mcp-image.yml |
| S3LC0026 | release feed | all, on release | yes | only when asked (--release, -p:SnailConformanceRelease=true): no LocalPackages source or mapping in nuget.config, no -local version in global.json msbuild-sdks or Directory.Packages.props |
| S3LC0027 | workflow inputs | all | yes | every call of a shared workflow or action (orldev/Snail.Sdk/...) names one Snail.Sdk keeps, passes only inputs it declares and every input it requires |
| S3LC0028 | release tag | all | no | publish.yml — an application's image.yml — starts on tags: ['v[0-9]+.[0-9]+.[0-9]+*'], which the moving v1 tag does not match — advice |
| S3LC0029 | job secrets | server with a launchd job | yes | the job template in deploy/ names no variable that holds a secret (a TOKEN, KEY, SECRET, PASSWORD or CREDENTIALS in its name): a secret goes into the server's settings file |
| S3LC0030 | image version | server or application with an image | yes | every Dockerfile the shared mcp-image.yml builds takes the version it passes: each stage that runs dotnet publish declares ARG VERSION, with a default for a local build, and publishes with -p:Version=$VERSION |
| S3LC0031 | translations | all | yes | every translation Name.<culture>.resx holds exactly the keys of the English Name.resx beside it, none empty, with the same format items ({0}) |
Making each check hold¶
The files the checks compare with — global.json, .editorconfig, the workflow callers, dependabot.yml,
nuget.config — are the ones dotnet new s3l-lib, dotnet new s3l-mcp and dotnet new s3l-web write (Getting
started); copying them from a fresh repository of the template is the quickest fix for most checks.
| Id | Why | How to fix |
|---|---|---|
| S3LC0001 | one solution format, the one the workflows find with find -name '*.slnx' |
dotnet sln migrate, then delete the .sln |
| S3LC0002 | the code style analyzers ship with the .NET SDK: another band applies other rules to the same code | copy the sdk section of Snail.Sdk's global.json (order, spacing and line ends do not matter; msbuild-sdks and comments are the repository's own). The detail names each differing property |
| S3LC0003 | one version of each package for every project, raised in one place | ManagePackageVersionsCentrally in Directory.Packages.props, versions as <PackageVersion> there, none on a <PackageReference> |
| S3LC0004 | the rules ship in the SDK; a rule left in .editorconfig would outrank the package and freeze it |
copy Snail.Sdk's .editorconfig; the detail names the first differing line |
| S3LC0005 | a package states its licence | add LICENSE |
| S3LC0006 | each package comes from the one source named for it | add packageSourceMapping to nuget.config |
| S3LC0007 | every rule of the standard comes through the SDK | put each project on a level: Sdk="Snail.Sdk.Library", or <Sdk Name="…" /> beside another Microsoft SDK |
| S3LC0008 | code without tests is not shipped | add a *.Tests project |
| S3LC0009 | performance is measured, not assumed | add a *.Benchmarks project with BenchmarkDotNet; a project of kind Tool does not count |
| S3LC0010 | perf.yml fails on a regression only against a baseline | copy the CSV reports of an accepted perf.yml run into baseline/ beside the benchmark project (benchmarks/baseline/ for a project directly in benchmarks/) |
| S3LC0011 | one CI for the fleet; a push only from a job nuget.org trusts | callers as in Workflows: the release job without id-token: write, the push job with it and the nuget-push action |
| S3LC0012 | the SDK, the workflows' tag and the packages are kept current | .github/dependabot.yml with version: 2 and the github-actions and nuget ecosystems, as the templates write it |
| S3LC0013 | the README is the site's home page and the package's page | open README.md with # <Name> |
| S3LC0014 | the reference on the site is the code's, and CI compares it | build with -p:SnailReference=true and commit docs/reference |
| S3LC0015 | every site looks the same and changes with the SDK | delete docs/theme, docs/requirements.txt and the theme: block of docs/mkdocs.yml |
| S3LC0016 | a silenced trim or AOT warning is a call that fails at run time in the native program | remove the IL codes from NoWarn and fix or annotate what they hid; a dependency's warnings the program never reaches are named in the CI's forgiven-native-warnings with IlcTreatWarningsAsErrors false; a server that cannot be native sets SnailMcpNative false |
| S3LC0017 | a NoWarn hides a rule from every new line too |
fix what it hides, or suppress the one place with [SuppressMessage] and a reason; the detail lists each file and its codes |
| S3LC0018 | every public symbol is written down, so an API change shows in the diff | add PublicAPI.Shipped.txt and PublicAPI.Unshipped.txt beside each packed project |
| S3LC0019 | a released package is validated against its last release | set SnailPackageBaseline to the last released version in Directory.Build.props. Released means a PublicAPI.Shipped.txt lists at least one API, or a v* tag of an earlier release exists. A tag on the commit checked out, or the one the workflow runs for, is the release being made: a first release passes without a baseline, which nuget.org could not give yet |
| S3LC0020 | IsAotCompatible sees warnings only; a probe proves the library binds in a native program |
add tests/<Name>.NativeProbe, or set SnailNotNativeBecause to the reason |
| S3LC0021 | the registry and clients install the server from it | $schema https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json; name <reverse DNS>/<…>-<short name>; a real version (the release overwrites it); every packages[].identifier the repository's name; one package entry per transport — stdio, streamable-http with its url, both for Both; every environment variable SNAIL_MCP_<SHORT>_… |
| S3LC0022 | the tool reference is grouped by families | add docs/tools.json, validated by the reference generator |
| S3LC0023 | the server is asked as a client asks it, and its tools pass the guard rules | a contract test through ServerProgram of Snail.Sdk.Mcp.Testing and a test calling ToolGuards.Problems |
| S3LC0024 | one place for a server's data in every home | name the folders with ServerFolders.DataDirectory/ConfigDirectory |
| S3LC0025 | an image nobody builds breaks unseen | a Dockerfile at the root and an image.yml calling mcp-image.yml |
| S3LC0026 | a release must build where only nuget.org exists | remove the LocalPackages source and mapping from nuget.config, and every -local version from global.json and Directory.Packages.props |
| S3LC0027 | GitHub refuses a call with an unknown or missing input when it runs, in a repository that built green | pass only the inputs listed in Workflows; the detail names the file, the line and the inputs |
| S3LC0028 | the moving v1 tag must start no release |
tags: ['v[0-9]+.[0-9]+.[0-9]+*'] in publish.yml |
| S3LC0029 | a secret in the template travels with the repository, and a job edited by hand is no longer restarted by an install | move it into ~/.config/s3l-mcp-<short>/config.json, which the server reads through ServerSettings, readable by its owner alone (installing) |
| S3LC0030 | an image tagged 1.2.0 whose server says another version in initialize cannot be told from any other build |
as the template writes it: ARG VERSION=0.0.0-local before the first FROM, ARG VERSION again in each stage that uses it, -p:Version=$VERSION on dotnet publish, and LABEL org.opencontainers.image.version=$VERSION in the final stage; the detail names the file and what is missing |
| S3LC0031 | a key missing from a translation falls back to English and shows only to a reader of that language; a placeholder lost in translation throws on the page that formats it | the detail names each file and key: add the key to the translation, or take it out of the English file too; keep {0} and its kin in every language. A language is the repository's choice: add its Name.<language>.resx, or delete it whole |
The reference generator¶
Snail.Sdk.Docs, which the SnailReference and SnailSite targets run, stops on one line with an id, which the build
shows as an MSBuild error. Exit codes: 0 written, 1 an error line, 2 a wrong command line.
| Id | Means | Fix |
|---|---|---|
| S3LD0001 | docs/tools.json is not valid: not JSON, an unknown key at the top or in a family, a family without a non-empty type or title, a slug outside [a-z0-9-]+, index as a slug, or a type or slug named twice |
keep to the keys intro (paragraphs), skillNote, openWorld, closedWorld (text) and families (each { "type", "slug", "title" }); // comments are allowed |
| S3LD0002 | docs/tools.json and the server disagree: a class declaring tools has no family, a family names a class that declares none, or two families land on one page |
one family per class that declares tools, and only those |
| S3LD0003 | an input cannot be read or an output written | the message names the file |