Skip to main content
Glama
kapio-hub

@kapio/mosaic-mcp

by kapio-hub

Mosaic MCP Kit

@kapio/mosaic-mcp is the shared frame for MCP servers that sign people in through a Mosaic instance. A server brings its tools; the kit brings everything around them:

  • Streamable HTTP transport: POST /mcp (JSON or SSE), sessions via Mcp-Session-Id, CORS, GET /health

  • Sign-in through Mosaic: Bearer access tokens checked against the instance JWKS (EdDSA, iss, aud, exp, nbf)

  • OAuth discovery (RFC 9728): /.well-known/oauth-protected-resource[/mcp] and WWW-Authenticate: Bearer resource_metadata="…" on 401

  • Static token ways for the transition and the emergency access, compared in constant time

  • One JSON log line per tools/call with the person, never arguments, results or token values

No runtime dependency: node:http and node:crypto, Node 20 or newer.

Use

const { createMcpServer } = require('@kapio/mosaic-mcp');
const McpHandler = require('./mcp-handler');

let app;
try {
  app = createMcpServer({
    service: 'kapio-harvest-mcp',
    version: '1.1.0',
    protocol: '2025-11-25',
    handler: new McpHandler(),
    extraHealth: () => ({ harvest: true }),
    tokens: [{ name: 'notzugang', value: process.env.MCP_ACCESS_TOKEN }]
  });
} catch (err) {
  console.error(`FATAL: ${err.message}`);
  process.exit(1);
}
app.listen(process.env.PORT || 8080);

Environment

Example

Required

OAUTH_ISSUER

https://mosaic.kapio.eu/api/auth

yes

MCP_RESOURCE

https://harvest-mcp.kapio.eu/mcp

yes

ALLOWED_ORIGINS

https://claude.ai (default)

no

Without OAUTH_ISSUER or MCP_RESOURCE, createMcpServer throws ConfigError and the server must not start.

Related MCP server: wazobiatech-nexus-mcp

API

createMcpServer(options)

Option

Meaning

service

name in health and log

handler

object with handleRequest(request, sessionId, identity); returns the JSON-RPC response or null (202)

version, protocol

shown in /health

extraHealth

function returning service fields for /health

tokens

[{ name, value, instances?, write? }], static token ways; entries without value are dropped

allowQueryToken

true accepts ?token= for static tokens; Mosaic tokens are only accepted by header

issuer, resource, allowedOrigins, jwks, log, env

overrides, mainly for tests

Returns { server, listen(port), close(), describe(), issuer, resource, metadataUrl }. describe() gives { service, version, protocol, resource, issuer, metadataUrl } for a registrar.

identity passed to the handler:

Way

Shape

Mosaic token

{ via: 'oauth', email, sub, azp, scope }

static token

{ via: '<token name>', scope: <the token entry without value>, query }

The server decides what scope.instances and scope.write allow; the kit only finds the entry.

verifyAccessToken(token, { issuer, resource, jwks, now? })

Resolves { sub, email, azp, exp, scope } or rejects with TokenError whose reason is one of malformed, alg, kid, signature, iss, aud, exp, nbf, jwks.

Rule

Check

algorithm

EdDSA only, key chosen by kid

issuer

iss equals the one configured issuer exactly

audience

aud (string or array) contains the resource

lifetime

exp required; exp and nbf with 60 s leeway

jwks is a client from createJwksClient({ uri }) or a plain { keys } document.

createJwksClient({ uri, fetch?, now?, ttlMs?, minRefreshMs?, fetchTimeoutMs? })

Caches the key set for ten minutes. An unknown kid reloads once, at most once per minute, then counts as unknown. With the issuer unreachable or the fetch hanging (aborted after fetchTimeoutMs, default 5000 ms), cached keys keep working and no more than one reload attempt runs per minute.

matchToken(given, tokens)

Returns the matching entry or null. Compares SHA-256 digests with timingSafeEqual against every entry without leaving early.

Call log

One line on stdout per tools/call:

{"ts":"2026-09-17T08:00:00.000Z","service":"kapio-harvest-mcp","event":"tools/call","tool":"harvest_list_projects","via":"oauth","ok":true,"email":"person@example.com","sub":"…","azp":"…"}

Static token ways log query: true|false and the user agent capped at 120 bytes instead of email/sub/azp.

Transition from URL tokens

  1. A server switches to the kit with allowQueryToken: true and keeps its static tokens.

  2. When the call log shows seven days without "query":true, allowQueryToken goes in its own commit.

  3. What stays: scoped tokens by header for automations without a person, and the emergency token by header (via: "notzugang").

Shared test cases

fixtures/verify-cases.json holds signed tokens with expected outcomes at a fixed clock. Other implementations (the Python agent template) run the same file and pin it by fixtures/verify-cases.json.sha256. Regenerate only on purpose with npm run fixtures.

Release

A tag v<version> matching package.json publishes to npm with provenance (.github/workflows/publish.yml).

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A reference implementation for creating an MCP server supporting Streamable HTTP & SSE Transports with OAuth authorization, allowing developers to build OAuth-authorized MCP servers with minimal configuration.
    107
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables building MCP servers with HMAC signing and verification, providing server scaffolding and middleware for secure request authentication.
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables developers to build MCP servers with registry-managed tool metadata, runtime hot-reloading, pluggable authentication and authorization, per-audience tool views, and resilient stateless operation.
    -
  • A
    license
    B
    quality
    A
    maintenance
    Provides a reference MCP server that implements every MCP feature so client, gateway, and runtime authors can test against a single endpoint, and publishes a service contract describing its capabilities.
    31
    1
    MIT