▶ adopter quickstart · v0.1.1

Portal — adopter quickstart.

You have a service with some HTTP endpoints. You want any LLM client to be able to visit it cold and call a tool. Portal is that, in two endpoints and one manifest. The fastest way to know you're conformant is to run runSmokeConformance against your live Portal and read the report. That's the first thing on this page.

30-second conformance check

If your service is already exposing GET /portal and POST /portal/call, here is the shortest path from zero to a pass/fail answer:

npm i @visitportal/spec

import { runSmokeConformance } from '@visitportal/spec';
const report = await runSmokeConformance('https://my-service.com/portal');
console.log(report);

runSmokeConformance is a smoke check — it validates the manifest against the JSON Schema and verifies a NOT_FOUND round-trip on POST /portal/call. It runs in under a second and is safe to hit a live service with. If it returns { ok: true }, the basics are right; adopters typically run this in CI against a staging URL.

The package is Apache 2.0 + CC0 dual-licensed and has zero runtime dependencies outside of ajv.

Full offline conformance

When you want the full suite — all 30+ vectors against a manifest literal, no network, deterministic — use validateAgainstVectors:

import { validateAgainstVectors } from '@visitportal/spec';
import manifest from './portal.json' assert { type: 'json' };

const report = validateAgainstVectors(manifest);
if (!report.ok) {
  console.error(report.failures);
  process.exit(1);
}

This is the flow we recommend for a pre-commit hook or CI check that runs on every manifest edit. Every failure entry cites the exact vector id and the schema rule it tests, so you can jump straight to the fix.

Manifest shape

A v0.1.1-conformant manifest is compact. Required keys: portal_version, name, brief, tools[], call_endpoint. Optional: auth, pricing.

{
  "portal_version": "0.1",
  "name": "My Service",
  "brief": "Natural-language description for the visiting LLM.",
  "tools": [
    {
      "name": "ping",
      "description": "returns pong",
      "params": { "msg": { "type": "string" } }
    }
  ],
  "call_endpoint": "https://my-service.com/portal/call",
  "auth": "none",
  "pricing": { "model": "free" }
}

Tool params accept the sugar form { type, required?, description? } for the 95% case, or a full JSON Schema via paramsSchema for the rest. The two forms are mutually exclusive per-tool. call_endpoint must be https:// with a loopback escape hatch for http://localhost and http://127.0.0.1 during development.

Error envelope — five codes

Every POST /portal/call response is either { ok: true, result } or { ok: false, error, code }. The code is one of five values; this is the entire surface your visitor needs to understand:

CORS (Appendix C)

For browser-resident visitors, Portal requires a short, normative CORS contract. Both endpoints MUST handle OPTIONS preflight and MUST set Access-Control-Allow-Origin. Credentialed requests have per-auth-mode semantics. See spec Appendix C for the full table.

Rate limits (Appendix D)

Portal SHOULDs a per-auth-mode default for rate limits. Visitor SDKs MUST treat RATE_LIMITED as recoverable and SHOULD honor Retry-After. Providers without a rate-limit strategy of their own can adopt the defaults verbatim. See spec Appendix D.

Framework snippet — Next.js App Router

A minimal Portal in Next.js 15 App Router is two route handlers. Other framework quickstarts (Hono, FastAPI, Express) are queued for v0.1.2.

// app/portal/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  return NextResponse.json({
    portal_version: '0.1',
    name: 'My Service',
    brief: 'What this service does, in plain English.',
    tools: [
      { name: 'ping', description: 'returns pong',
        params: { msg: { type: 'string' } } },
    ],
    call_endpoint: `${process.env.PORTAL_PUBLIC_URL}/portal/call`,
    auth: 'none',
    pricing: { model: 'free' },
  });
}

// app/portal/call/route.ts
import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  const { tool, params } = await req.json();
  if (tool === 'ping') {
    return NextResponse.json({
      ok: true,
      result: { pong: true, msg: params?.msg },
    });
  }
  return NextResponse.json({
    ok: false,
    error: `tool '${tool}' not in manifest`,
    code: 'NOT_FOUND',
  });
}

Visitor SDK — @visitportal/visit

On the calling side, the visitor SDK is three lines:

import { visit, CallFailed } from '@visitportal/visit';

const portal = await visit('https://my-service.com/portal');
const result = await portal.call('top_gainers', { limit: 3 });

Error taxonomy: PortalNotFound, ManifestInvalid, ToolNotInManifest, CallFailed. CallFailed.code is typed as the five-code enum above. SDK bundle is 2.25 kB gzipped with zero runtime dependencies.

Spec — at a glance

Full text: docs/spec-v0.1.1.md (public domain). One page of core plus four appendices — the A/B appendices from v0.1.0 and the new normative CORS (C) and SHOULD-level rate-limit (D) appendices added in v0.1.1.

SectionWhat it pins
§3 EndpointsGET /portal returns a manifest; POST /portal/call takes { tool, params }, returns { ok, result } or { ok: false, error, code }.
§4 ManifestRequired: portal_version, name, brief, tools[], call_endpoint. Optional: auth, pricing.
§6 Error codesNOT_FOUND · INVALID_PARAMS · UNAUTHORIZED · RATE_LIMITED · INTERNAL.
§7 Non-goalsNo task lifecycles (use A2A). No stateful sessions. No server-push, no streaming, no multi-agent. Those arrive as Portal Extensions.
Appendix CNormative CORS contract for browser-resident visitors.
Appendix DSHOULD-level rate-limit defaults + Retry-After guidance.

Developer tools in the monorepo

These are not adopter-facing. If you cloned the Portal monorepo to hack on Portal itself, you'll find packages/visit/ts/scripts/reference-demo.ts — a driver script that starts reference/trending-demo, visits it, and exercises the visitor SDK end-to-end. It's useful for developing the SDK; it's not needed to adopt Portal in your own service. Adopters should use runSmokeConformance and validateAgainstVectors above.