Skip to content

v2.0.0 - Dual-era: serve 2025 and 2026-07-28 MCP clients from one endpoint

Choose a tag to compare

@rinormaloku rinormaloku released this 29 Jul 10:10
· 2 commits to main since this release

v2 is a ground-up rewrite of how MCP-Nest plugs into NestJS, and it delivers
the two things people kept asking for: serve the newest MCP clients without
thinking about it, and build MCP servers the way you already build NestJS apps —
guards, interceptors, pipes, filters, and DI, on every tool.

Upgrading an existing v1 server? It's a breaking change — jump to
Upgrading from v1.


Highlights

🕰️ Modern-era MCP clients work out of the box

MCP shipped a revision — 2026-07-28 — that deletes the initialize
handshake and protocol sessions entirely. It's a clean break with no deprecation
period: old clients and new clients literally cannot talk to the same
single-era server.

MCP-Nest serves both eras concurrently, on one endpoint, by default — and
your tool code doesn't change at all. A 2024 client and a 2026 client can hit
the same /mcp in the same process, and each gets served correctly.

new StreamableHttpTransport();                            // dual-era — the default
new StreamableHttpTransport({ protocol: 'modern-only' }); // 2026-07-28 only
new StreamableHttpTransport({ protocol: 'legacy-only' }); // pre-2026 behaviour

There's nothing to configure. Era is a per-request fact, decided by whoever
called you.

🧩 Your tools are first-class NestJS handlers

This is the big one. Every @Tool, @Resource, @ResourceTemplate, and
@Prompt is now a real @MessagePattern handler on an @McpController(), so
the entire NestJS pipeline applies natively — no bespoke machinery bolted on
top:

@McpController()
@UseGuards(ApiKeyGuard)                  // gate the whole capability class
@UseInterceptors(MetricsInterceptor)     // time and log every tool call
export class OrdersController {
  constructor(private readonly orders: OrdersService) {} // real DI, real services

  @Tool({
    name: 'refund-order',
    description: 'Refunds an order',
    parameters: z.object({ id: z.string() }),
  })
  @ToolScopes(['orders:write'])          // per-tool authorization
  @UseFilters(DomainErrorFilter)         // your own error mapping
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  async refund(@Payload() { id }: { id: string }, @Ctx() ctx: McpContext) {
    await ctx.reportProgress({ progress: 1, total: 2 });
    const receipt = await this.orders.refund(id);
    return { content: [{ type: 'text', text: `Refunded ${receipt.id}` }] };
  }
}

class-validator DTOs, custom pipes, request-scoped providers, @UseFilters,
RpcException — all of it just works, because these are ordinary Nest RPC
handlers now.

🔓 Bring your own schema library

parameters and outputSchema are no longer Zod-only. Any Standard Schema
validator that emits JSON Schema works — Zod, ArkType, Valibot — or hand it a
raw JSON Schema object:

@Tool({ name: 'add', description: 'Adds two numbers', parameters: type({ a: 'number', b: 'number' }) })   // ArkType
@Tool({ name: 'echo', description: 'Echoes', parameters: { type: 'object', properties: { text: { type: 'string' } } } }) // raw JSON Schema

Existing Zod code is untouched — this is a widening, not a migration.

🛣️ Own the MCP route like any other controller

McpHttpControllerFor(transport) turns a transport into a real Nest controller
you own, with the full HTTP pipeline — guards, interceptors, filters,
versioning, middleware:

@Controller('mcp')
@UseGuards(McpAuthJwtGuard)
@UseInterceptors(HttpTimingInterceptor)
export class McpHttpController extends McpHttpControllerFor(mcpTransport) {}

That's also where authentication belongs now — a normal Nest guard, not a
module option.

🏢 Several MCP servers in one app, and tools registered at runtime

Named servers give each endpoint its own isolated tool set
(@McpController({ server: 'admin' }) + McpStrategy({ server: 'admin' })),
and the strategy can register and remove tools, resources, and prompts at
runtime — now emitting proper listChanged notifications so connected clients
refresh.

🔐 A hardened built-in OAuth server

@rekog/mcp-nest-auth (now its own package, so core stays free of typeorm,
passport, and @nestjs/jwt) gained real security work: token
audience/issuer/type validation, PKCE S256 enforcement, scope narrowing at
/authorize, an opt-in consent screen, Client ID Metadata Documents with an
SSRF-guarded fetcher, and RFC 9207 iss on authorization responses.


v1 → v2 at a glance

v1 (1.9.x) v2 (2.0.0)
How MCP plugs into Nest McpModule.forRoot() generating HTTP controllers a NestJS microservice CustomTransportStrategy (McpStrategy)
Where tools live @Injectable() classes in providers @McpController() classes in controllers
Guards / pipes / interceptors / filters on tools bespoke, partial native NestJS — tools are real @MessagePattern handlers
Transports HTTP+SSE, Streamable HTTP, STDIO Streamable HTTP, STDIO (HTTP+SSE removed)
Protocol revisions 2025-era only dual-era: 2025-era and the stateless 2026-07-28 revision on one endpoint
OAuth authorization server bundled in core separate package @rekog/mcp-nest-auth
MCP SDK @modelcontextprotocol/sdk v1 @modelcontextprotocol/{core,node,server} v2

Verified on main at the time of writing: 851 unit/integration tests pass, 0
fail
(56 files, both protocol eras). The example-driven e2e/ suite — which
boots every examples/<project> as a real subprocess and drives it with a
pinned old client (@modelcontextprotocol/sdk@1.10.0) and a modern
2026-07-28 client — last ran 316 pass / 0 fail across 16 files. The old
client passing unmodified is the backward-compatibility proof for 2025-era
consumers.


Installation

npm install @rekog/mcp-nest \
  @modelcontextprotocol/server \
  @modelcontextprotocol/core \
  @modelcontextprotocol/node \
  @nestjs/microservices \
  zod@^4
  • The three MCP SDK packages are peer dependencies. 2.0.0-beta.5 is the
    minimum — it is the first SDK release matching the final 2026-07-28
    wire format. beta.4 ships the pre-final shape and is interop-broken against
    conforming peers.
  • The declared range ^2.0.0-beta.5 accepts later 2.0.0-beta.x, 2.0.0-rc.x
    and the eventual stable 2.0.0/2.x. It does not accept prereleases of a
    later line (e.g. 2.1.0-beta.1) — that would need a range edit in a future
    MCP-Nest release.
  • @nestjs/microservices is a new required peer dependency — it is how the
    strategy attaches to your app.
  • Optional: npm install @rekog/mcp-nest-auth for the built-in OAuth
    authorization server, plus @nestjs/typeorm typeorm if you use its TypeORM
    store.

Upgrading from v1

v2 is not backward compatible. Every v1 server needs code changes — there is no
compatibility shim. The fastest path is to hand the migration to your coding
agent, then test against a real MCP client before shipping.

Upgrading with an agent

The migration is mechanical but touches every capability class and your
bootstrap. It is well suited to an agent, and the repo ships the guide the agent
needs.

1. Point the agent at the migration guide. It is the authoritative old→new
API map:

2. Give it live docs via Context7. MCP-Nest is indexed at
context7.com/rekog-labs/mcp-nest
connect the Context7 MCP server
so the agent can pull current v2 documentation instead of recalling v1 APIs from
training data.

3. A prompt that works well — copy this verbatim:

Upgrade this NestJS app from @rekog/mcp-nest v1 to v2.

Read https://github.com/rekog-labs/MCP-Nest/blob/main/docs/migration-to-v2.md
first, and use Context7 (rekog-labs/mcp-nest) for anything the guide does not
cover. Do not rely on your memory of this library — v1's McpModule.forRoot()
API does not exist in v2 and will not compile.

Specifically:
- update dependencies as described in the guide's section 0: drop
  @modelcontextprotocol/sdk, add @modelcontextprotocol/{core,node,server} and
  @nestjs/microservices, and add @rekog/mcp-nest-auth (+ cookie-parser) if the
  app uses the built-in OAuth server;
- replace McpModule.forRoot(...) with a new McpStrategy({ ... }) built in
  main.ts, wired with setHttpAdapter -> connectMicroservice ->
  startAllMicroservices() BEFORE listen();
- move every @Tool/@Resource/@ResourceTemplate/@Prompt class from providers to
  controllers and decorate it with @McpController();
- convert positional (args, context, request) signatures to
  @Payload() / @Ctx() ctx: McpContext / ctx.getRawRequest();
- move OAuth imports to @rekog/mcp-nest-auth and mount app.use(cookieParser())
  if McpAuthModule is used;
- replace the module-level guards option with McpHttpControllerFor(transport)
  + @UseGuards().

Then build, run the test suite, and list anything you could not migrate
mechanically.

Test before you go to production

Please do not upgrade a production server without exercising it first. v2
rewrote the request path end to end — the transport layer, the handler dispatch,
the error masking, the auth surface, and the protocol negotiation. The suites
above are extensive, but they cannot cover your combination of guards,
interceptors, custom controllers, IdP, and client.

A reasonable pre-production checklist:

  1. Connect a real MCP client (Claude Code, MCP Inspector, your own) and
    confirm tools/list, tools/call, resources/*, and prompts/* all behave
    as they did on v1.
  2. Exercise every guard and auth path. Authentication moved to guards on an
    HTTP controller you own; per-tool authorization runs inside the RPC pipeline.
    Check both an authorized and an unauthorized caller, and check that
    tools/list is filtered the way you expect.
  3. Check your error messages. Unknown errors are now masked to a generic
    "Internal server error" by NestJS's RPC exception handler. If your clients
    relied on seeing the real message, register McpExceptionFilter or throw
    RpcException (see Behavioral changes).
  4. Check statefulness. Streamable HTTP is now stateless by default
    if you relied on sessions, progress, or server-initiated notifications, you
    need { statefulMode: true } on the legacy era.
  5. Test both protocol eras if your clients are mixed. A plain SDK v2 client
    still negotiates the legacy era by default; pin 2026-07-28 to actually
    exercise the modern leg (see
    docs/protocol-revisions.md).
  6. If you use serverMutator, elicitation, or sampling — verify against the
    era you actually serve. Push-style server→client requests throw on
    2026-07-28 (see Known gaps).
  7. Run your own integration tests against a built artifact, not just a
    type-check. Several classes of bug in this rewrite were only visible on the
    wire.

If something is wrong, please open an issue — this is a large change and
unintended side effects are possible.


Everything new, in detail

Core architecture

  • NestJS microservice transport strategy. MCP runs as a real
    CustomTransportStrategy. Every tool, resource, and prompt is a
    @MessagePattern handler on an @McpController() class.
  • The full NestJS pipeline applies to MCP calls@UseGuards,
    @UsePipes, @UseInterceptors, @UseFilters, request-scoped DI, and
    class-validator DTOs all work natively on tools, resources, and prompts.
  • McpExceptionFilter (exported) un-masks handler errors to the agent when
    you want that, and RpcException passes through unmasked so you can
    surface a clean, client-facing message.
  • McpHttpControllerFor(transport) — a mixin that turns a transport into a
    real, ownable Nest controller with the full HTTP pipeline (guards,
    interceptors, filters, versioning, middleware). Replaces the old
    McpStreamableHttpService hand-wiring, and is where HTTP-layer authentication
    now belongs.
  • Named servers replace forFeature: @McpController({ server: 'name' }) +
    McpStrategy({ server: 'name' }) for several isolated MCP servers in one app.
  • Dynamic capabilities on the strategy
    registerTool/registerResource/registerPrompt and matching
    removeTool/removeResource/removePrompt, now emitting listChanged
    notifications.
  • @McpRawRequest() param decorator (core) and @McpUser() (auth
    package).
  • Resource templates: query parameters ({?a,b}) are supported and the
    catch-all wildcard ({path*}) — broken in v1 — now works.

Schema flexibility

  • @Tool's parameters and outputSchema accept any Standard Schema
    validator that can emit JSON Schema
    (Zod 4.2+, ArkType 2.1+, Valibot via
    @valibot/to-json-schema) or a raw JSON Schema object. Zod is no longer a
    hard requirement of the core package. Existing Zod code is unchanged.
  • Tool schemas are now advertised in the JSON Schema 2020-12 dialect (was
    draft-07). This matters: SDK v2 clients compile outputSchema as 2020-12 and
    reject any other declared $schema client-side, so a draft-07 tool with an
    output schema was uncallable by a conforming modern client.
  • outputSchema is no longer force-cast to type: 'object' — array and scalar
    output schemas survive intact (but see the structuredContent caveat in
    Known gaps).

Protocol revision 2026-07-28 (dual-era serving)

One /mcp endpoint answers both the 2025-era protocol (initialize + sessions)
and the new stateless 2026-07-28 revision, concurrently, with no change to
your tool code
. Era is a per-request fact, chosen by the client.

new StreamableHttpTransport();                            // dual-era (default)
new StreamableHttpTransport({ protocol: 'modern-only' }); // 2026-07-28 only
new StreamableHttpTransport({ protocol: 'legacy-only' }); // pre-2026 behaviour
new StdioTransport({ legacy: 'reject' });                 // stdio, modern openings only

New surface that comes with it:

  • ctx.getProtocolVersion() / getClientCapabilities() / getClientInfo()
    — per-request client identity (modern era; undefined on legacy).
  • ctx.getSession().era'legacy' | 'modern'.
  • ctx.getTraceContext() — W3C traceparent/tracestate/baggage from
    _meta; works on both eras.
  • Progress and logging are no longer a session privilege.
    ctx.reportProgress() and ctx.log.* work on every modern request (they ride
    the request's own response stream, which auto-upgrades to SSE). On the modern
    era, logging is opt-in per request via io.modelcontextprotocol/logLevel
    a tool's ctx.log.info() produces no client output unless the caller asked
    for it. That is spec-mandated, not a dropped message.
  • responseMode: 'auto' | 'sse' | 'json' — the modern-era counterpart of
    enableJsonResponse.
  • cacheHints on McpStrategy — set ttlMs/cacheScope per cacheable
    method. The SDK's default { ttlMs: 0, cacheScope: 'private' } means no
    client ever caches your tool list
    . Note that cacheScope: 'public' is a
    data-sharing decision; MCP-Nest warns at startup if you set it on a
    tools/list that is filtered per caller.
  • security: { allowedOrigins, allowedHosts }Origin/Host validation
    with a 403 + JSON-RPC body. Off unless you configure an allowlist (the
    library cannot know which hostnames your deployment answers on).
  • stepUpAuthorization (opt-in) — HTTP 403 +
    WWW-Authenticate: error="insufficient_scope" for per-tool scope failures,
    instead of an in-band JSON-RPC error.
  • Spec error-code fixes: unknown tool / prompt / resource now return
    -32602 InvalidParams, not -32601 (which is reserved for "method not
    implemented" and is load-bearing for client era detection).

Full detail: docs/protocol-revisions.md.

@rekog/mcp-nest-auth (the OAuth server, now its own package)

Moving it out keeps core free of typeorm, passport, and @nestjs/jwt. Its
v1 behaviour carries over, and v2 adds substantial security hardening:

  • Token audience / issuer / type validation. Previously the only
    check was the HS256 signature — a token minted by the same authorization
    server for a sibling resource, a refresh token, or a browser-cookie token all
    authenticated as bearer credentials. ⚠️ Breaking for deployments whose
    configured resource doesn't match the aud they mint
    , or whose tokens
    predate a jwtIssuer/serverUrl change: those now 401.
  • PKCE required, S256 only (requirePkce: false is the escape hatch;
    plain is not re-advertised).
  • Requested scopes are narrowed at /authorize against the union of your
    configured scope lists and every @ToolScopes() declared scope.
    scopeValidation: 'passthrough' opts out.
  • Canonical issuerjwtIssuer (defaulting to serverUrl) is the single
    issuer used everywhere. A jwtIssuer ≠ serverUrl config now throws at
    bootstrap
    .
  • iss on authorization responses (RFC 9207) + the paired metadata flag.
  • Consent screen — opt-in (consent: { enabled, render, rememberForMs }),
    CSRF-protected, HTML-escaped, showing the client name, the user, the
    redirect-URI hostname, and the narrowed scopes.
  • Client ID Metadata Documents (CIMD) — opt-in, SSRF-guarded (DNS pinned to
    a vetted public address, no redirects, size/time caps), bounded LRU cache,
    snapshot-at-consent semantics. Forces the consent screen on, because the
    "MUST display the redirect URI hostname" requirement cannot otherwise be met.
  • disableEndpoints.register for CIMD-only deployments; the
    registration_endpoint is then omitted from metadata rather than advertised
    and 404ing.
  • offline_access dropped from protected-resource scopes_supported (new
    SHOULD NOT), and scopes_supported is omitted rather than sent as [].
  • mcpVersionsSupported now defaults to ['2026-07-28', '2025-06-18'],
    matching the default dual-era posture.
  • Missing cookie-parser is now a boot failure, not a confusing
    400 Missing OAuth session at the last leg of the handshake. Mount
    app.use(cookieParser()) in your bootstrap; skipCookieParserCheck: true
    opts out if you use an equivalent parser the check can't recognise.

Breaking changes

API map

v1 v2
McpModule.forRoot({...}) new McpStrategy({ name, version, transports: [...] })
McpModule.forRootAsync({ useFactory }) build the strategy in your own async bootstrap() before connectMicroservice
McpModule.forFeature([...], 'name') @McpController({ server: 'name' }) + McpStrategy({ server: 'name' })
@Injectable() tool class in providers @McpController() class in controllers
(args, context, request) positional params @Payload(), @Ctx() ctx: McpContext, ctx.getRawRequest() / @McpRawRequest()
transport: McpTransportType[] transports: McpTransport[] instances
new StreamableHttpTransport({ statelessMode: true }) new StreamableHttpTransport() (stateless is the default)
new StreamableHttpTransport({ statelessMode: false }) new StreamableHttpTransport({ statefulMode: true })
McpRegistryService.registerTool() strategy.registerTool() (inject via MCP_STRATEGY)
McpStreamableHttpService + hand-written controller class X extends McpHttpControllerFor(transport)
McpModule.forRoot({ guards }) @UseGuards() on the McpHttpControllerFor controller
@ToolGuards() native @UseGuards()
McpAuthModule, McpAuthJwtGuard, McpUser, providers, stores from @rekog/mcp-nest same symbols from @rekog/mcp-nest-auth

Removed

McpModule (forRoot/forRootAsync/forFeature), McpRegistryService,
McpRegistryDiscoveryService, the createStreamableHttpController /
createSseController factories, StdioService, the SseTransport
(HTTP+SSE) transport
, the guards option on the strategy, the
@ToolGuards() decorator, and the module options transport, apiPrefix,
sseEndpoint, messagesEndpoint, mcpEndpoint, and streamableHttp.

There is no apiPrefix/global-prefix mechanism for MCP anymore — set the
transport's endpoint directly (a deeper path like /api/service/mcp works the
same way).

Behavioral changes to watch for

  • Streamable HTTP is stateless by default, and enableJsonResponse now
    defaults to the session mode (JSON in stateless, SSE in stateful) instead of
    always false.
  • Unknown errors are masked. A tool throwing a plain Error (or an
    McpError) returns { isError: true } with a generic "Internal server error",
    because NestJS's RPC exception handler masks unknown errors before the
    strategy sees them. Input problems are not masked — schema validation
    returns a clear Invalid parameters: …. To surface your own message, throw
    RpcException or register McpExceptionFilter.
  • Request scoping: @Inject(REQUEST) in a request-scoped tool resolves to
    the RPC request context, not the raw HTTP request. Use ctx.getRawRequest().
  • sessionIdGenerator, enableJsonResponse, statefulMode, GET/DELETE /mcp are all legacy-era only. The 2026-07-28 revision removed sessions
    outright.
  • Push-style server→client requests are legacy-only.
    elicitInput(...), createMessage(...) (sampling), listRoots(...) and
    ping(...) throw on a 2026-07-28 request. They keep working for 2025-era
    clients on a dual-era server.
  • NestJS versioning is unaffected — the MCP endpoint stays
    VERSION_NEUTRAL alongside versioned REST routes.
  • STDIO servers must disable logging (logging: false + { logger: false })
    — stdout carries the protocol. Create them with
    NestFactory.createMicroservice(AppModule, { strategy }), and remember that
    capability classes must be @McpController() in controllers (v1-style
    providers are not discovered, so tools/list comes back empty).

Modern MCP spec (`2026-07-28`) — what's supported

Supported

Area Status
Dual-era serving on one endpoint ✅ default (protocol: 'dual')
server/discover (replaces initialize) ✅ incl. supportedVersions, capabilities, _meta serverInfo
Per-request _meta envelope (protocol version, client capabilities, client info) ✅ + exposed on McpContext
Stateless request model
Tools / Resources / Resource templates / Prompts on the modern era ✅ same handlers, both eras
Progress + logging on sessionless requests ✅ via the request's own response stream
io.modelcontextprotocol/logLevel per-request logging opt-in ✅ (spec MUST NOT enforced)
subscriptions/listen ✅ served by the SDK entry (in-process event bus)
listChanged notifications for dynamic tools/resources/prompts
Mcp-Method / Mcp-Name headers + header↔body cross-check (-32020)
ttlMs / cacheScope cache hints ✅ configurable via McpStrategy({ cacheHints })
resultType on results ✅ (SDK-stamped)
JSON Schema 2020-12 dialect
Spec error codes (-32602 for unknown tool/prompt/resource, -32022, -32020)
Never an empty error body (400/403/404/405 all carry JSON-RPC errors)
Origin / Host validation → 403 ✅ opt-in allowlist
W3C trace context (traceparent/tracestate/baggage) _meta keys ✅ both eras
Deterministic tools/list order
X-Accel-Buffering: no on SSE, listen-stream keep-alives ✅ (SDK)
extensions on client/server capabilities ✅ passthrough
Pagination ✅ single page, no nextCursor (valid per spec)
stdio dual-era (probe → modern, initialize → legacy)
Fastify and Express adapters on both eras
Authorization: RFC 8707 resource, PKCE S256, token audience validation, WWW-Authenticate + resource_metadata, exact redirect-URI matching, RFC 9207 iss, CIMD, consent, step-up 403 ✅ (@rekog/mcp-nest-auth)

Not supported / not implemented

Gap Detail
Multi Round-Trip Requests (MRTR) The modern-era replacement for elicitation/sampling. Not wrapped. elicitInput/createMessage/listRoots/ping throw MethodNotSupportedByProtocolVersion on a 2026-07-28 request; they still work on the legacy leg. Gate on ctx.getSession().era === 'legacy' or serve that endpoint legacy-only.
Tasks extension (io.modelcontextprotocol/tasks) Not implemented.
Full SEP-2243 x-mcp-header support Header/body mirroring is refused at registration (a tool declaring it throws at startup / on registerTool) rather than implemented. Base64 sentinel decoding, static-reachability rules and numeric comparison are not done.
subscriptions/listen across processes The default event bus is in-process only. A user-suppliable ServerEventBus transport option is not yet exposed, so multi-replica deployments only notify listeners on the replica that made the change.
Non-object structuredContent round-trip The server side is correct (SEP-2106 honoured, array/scalar outputSchema advertised properly), but @modelcontextprotocol/client@2.0.0-beta.5 still types structuredContent as a record and rejects non-object values client-side. Blocked on the SDK client.
private_key_jwt / mTLS client authentication CIMD documents declaring them are refused at /authorize with a clear message. No JWKS fetching or JWT-assertion verification.
CIMD cache & remembered consents across processes Per-replica, in memory. Consequence is extra document fetches and extra consent prompts — not a correctness or safety issue.
CIMD trust policies (the spec's MAY) No domain allowlist, reputation, domain-age or certificate checks. Any routable https origin is accepted.
Consent on STDIO Not wired (no browser). No "revoke my approvals" route either (ConsentService.clearConsents() exists but is unexposed).
logo_uri rendering on the consent page Parsed but deliberately never rendered — it would be an attacker-controlled outbound request from the consent page.
code_challenge format validation (RFC 7636's 43–128 chars) Not validated; a malformed S256 challenge simply never verifies.

Deprecated upstream, still working

Roots, Sampling, and Logging are deprecated in 2026-07-28 with a ≥12-month
window, and Dynamic Client Registration is deprecated in favour of CIMD with
removal not eligible before 2027-07-28. All remain functional in MCP-Nest —
plan the migration, don't rush it.

One thing that surprises people

A stock SDK v2 client — MCP Inspector included — reports a dual-era server as
legacy 2025-11-25.
The client's default versionNegotiation is 'legacy':
no probe, no new headers. The era is chosen by the client, per request; the
server never prefers one. To actually exercise the modern leg you must pin
2026-07-28 or opt into mode: 'auto'.


v1 status

v1 lives on the v1 branch and is in security-patch-only maintenance:
community-contributed security fixes are accepted, but no new features or
general bug fixes from maintainers. Existing v1 users can stay on 1.9.x as
long as they need. New projects should start on v2.


Credits

The v2 rewrite was started by @Manuel-Antunes.

Please file issues for anything that breaks, feels wrong, or is missing from the
migration guide.