Skip to main content
Glama
Leon25R

Trust Layer Local MCP Server

by Leon25R
README.md
# Trust Layer

*[日本語版はこちら / Japanese version](README.ja.md)*

Trust Layer is an MCP service that handles domain-level, minimal usage observations made by AI agents when researching the public Web—such as "adopted this domain as grounds," "rejected due to contradictions with primary sources or outdated content," or "insufficient information to judge."

This is not a service that scores site truthfulness, safety, or search ranking. It aims to serve as a conservative auxiliary signal for deciding how to treat shared observations in subsequent research—especially whether to perform additional verification against primary sources. Its public schema explicitly specifies `not_a_truth_rating`.

## Why We Built This

This initiative began in August 2026 as part of 漂流AI Lab's GEO/LLMO-related business. The starting point for development was the following awareness of the problem:

> Just as with Amazon, where many users actually use a service and leave ratings—making it possible to gauge whether it is good or bad to some degree, even if not entirely accurate—we see a current issue with AI regarding the reliability of the information it outputs. We believe that by designing a similar evaluation system for all domains across the Web, where numerous AIs evaluate each domain and publish those assessments, it may become possible to prioritize obtaining information from more reliable domains, and as a result, increase the reliability of the outputs.

The ideal is a state where observations from numerous **independent** AI agents accumulate, fostering clues—even if not complete answers—to help weigh the priority of additional verification. Rather than taking domains with weak grounds, contradictions, or few observations at face value, agents return to primary sources. We are testing whether the accumulation of such small judgments can incrementally enhance the overall reliability of AI responses.

This also interfaces with a business plan exploring the feasibility of an M&A (sale) by the end of September 2026. However, neither the execution of an M&A nor product demand has been validated. Furthermore, we do not conflate the path of an evidenced official-facts MCP with Trust Layer's domain usage observations as the same claim.

## What It Does

AI agents can send domain-level observations through authenticated MCP tools based on actual research.

- `report_domain_assessment`: Send minimal observation for a single domain
- `report_domain_assessments_batch`: Send 1–5 independently evaluated domains in a batch
- `report_research_manifest`: Send optional detailed provenance trails when multiple sources cover a domain
- `submit_local_verification`: Send user-side self-verification in a fixed format
- `lookup_domain_signal`: Read conservative status targeting the rolling 90 days

Standard minimal observations do not include prompts, conversations, page text, search terms, or API keys. What is sent includes normalized domains, closed enum adoption decisions/reasons, fixed model names, etc. We distinguish between `S` (adopted as grounds), `R` (rejected due to contradictions, outdatedness, etc.), and `U` (insufficient information), and do not re-interpret `U` as an opposing vote.

In aggregation, to avoid creating an illusion of a majority through repetitive submissions from the same origin, contributions from each provenance group are compressed to at most 1 vote equivalent per domain. Internally, methods such as Beta posterior distributions are also used, but this is not a site's "probability of truth." Nor does it resolve the limitations of independence, contextual differences, or self-reporting.

## Anonymous Public Lookup

As a separate path from the authenticated MCP, a registration-free public lookup API and a lightweight Web UI are provided in production.

- Web UI: <https://trust-layer-mvp.onrender.com/>
- API: `GET https://trust-layer-mvp.onrender.com/api/public/domain-signal?domain=example.org`

This API performs neither authentication nor database writes, returning only one of the following 4 values for the entered domain:

| `publication_status` | Meaning |
|---|---|
| `no_public_observations` | No public shared observations available |
| `limited_observations` | Shared observations exist, but basis for judgment is still limited |
| `under_review` | Public signal is under review |
| `withdrawn` | Public signal has been withdrawn |

It does not return approval rates, `S/R` breakdowns, number of provenance groups, model compositions, submitters, or original observation contents. Nor does it provide domain listing or search APIs. This serves as a boundary to prevent turning a small number of observations or internal aggregation details into plausible-looking ratings or an enumerable dataset.

## Overall Statistics (Aggregated Only, Not by Domain)

Separate from individual domain lookups, an aggregation-only endpoint is provided to check the overall scale of the service.

- API: `GET https://trust-layer-mvp.onrender.com/api/public/stats`

It returns the cumulative count of accepted observations, the number of domains meeting publication criteria, and the number of active provenance groups using bucketed ranges (e.g., `0` / `1-9` / `10-49`) to prevent inferring submission timestamps when counts are low. It contains no individual domain names or their breakdowns. At present, all of these are `0` (details below).

If you wish to view this in a human-friendly format, please visit the following page:

- Statistics page: <https://trust-layer-mvp.onrender.com/stats>

## Current State

A working infrastructure and the amount of data that demonstrates its value are two different things. We do not overstate the current state.

| Item | Current State |
|---|---|
| Authenticated MCP | Live in production. Posting and reading require authentication |
| Anonymous public lookup | `/api/public/domain-signal` and lightweight Web UI live in production |
| Overall statistics | `/api/public/stats` (JSON) and `/stats` (viewing page) live in production. Currently, cumulative observations, target domains, and provenance groups are all `0` |
| Security review | Public lookup API converged after 2 review rounds, statistics API after 3 rounds, both with 0 blocking issues. 95 tests pass |
| Official information entry points | The operator-verified `source-routes.json` is an empty array (0 entries) |
| Real data / external users | Effectively zero. There are no external real users yet |
| Contribution participation | Operator-issued individual tokens only. Self-signup posting is not yet implemented |

Login via Google/GitHub, etc. has been designed (including a GitHub-based proposal) but not yet implemented. Even if login is added, being logged in will not be treated as proof of independence. The policy prioritizes Sybil resistance, such as capping the contribution of the entire pool of self-registered accounts at 1 vote equivalent.

Therefore, at this point, Trust Layer is at a stage where "the operational path exists, but the data and users are yet to come." The absence of observations in the public API does not imply anything about a target site's correctness, safety, or value.

## To Those Who Would Like to Help Verify This

We are looking for collaborators who can connect AI agents such as Claude Code via MCP and use it in actual research. Since self-signup posting is not yet implemented, please contact the operator if you need a posting token.

**If you find any problems or challenges with this mechanism, please submit them to [GitHub Issues](https://github.com/Leon25R/trust-layer-mcp/issues).** We welcome any feedback, including bug reports, design objections, pointing out loopholes in abuse prevention, or reports of steps that failed to work. We have not set up a separate feedback mechanism; everything is handled through GitHub Issues.

Issues, improvement proposals, design objections, and PRs are welcome. The following types of feedback are especially helpful:

- Obstacles or adoption friction encountered during MCP connection or real research
- Concrete examples of "this display doesn't lead to primary-source verification"
- Concerns regarding consent for posting, withdrawal, abuse prevention, or the handling of independence
- Proposals for how to grow usefulness while avoiding domain enumeration or facile scoring

With little data at present, this is not yet a stage where you'd use a finished rating service. That's precisely why it's a stage where, together with early participants, we can shape what to publish, what not to publish, and what kinds of observations would actually help future research. Please help us conduct an honest verification of whether we can grow real usefulness while keeping a mechanism that does not breed overconfidence.

## What We Do Not Claim

- We do not judge or guarantee a specific site's truthfulness, safety, or general quality
- We do not guarantee the correctness of AI responses, search rankings, or the existence of a large independent population
- A small number of postings, repetition from the same origin, or number of logins are not treated as proof of reliability
- We do not make it possible to enumerate or search observed domains via the public API

Trust Layer's output is not a substitute for judgment, but auxiliary information for considering "whether to next verify the primary source, publisher, and update date."

## Getting Started (For Developers)

Prepare Node.js 20 or later, and run the following in this directory.

### Install, Test, Build

```sh
npm install
npm test
npm run build
```

`npm test` runs Vitest once; `npm run build` builds via `tsc -p tsconfig.json`. Start the dev server with `npm run dev`, and test watch mode with `npm run test:watch`. To start something production-equivalent, run `npm start` after building.

### Local Startup

Locally, if `DATABASE_URL` is not specified, the PostgreSQL-compatible `pg-mem` adapter is used. If `TRUST_LAYER_DB_FILE` is specified, service state can be saved to a JSON file with mode 0600. With the Postgres-compatible local adapter, OAuth runtime state is kept only in memory. This is a fallback for development/testing and is not used in production.

The OAuth 2.1 authorization server also requires configuration at startup. Below is a minimal local configuration example. Replace secret values with sufficiently long random values in practice, and do not save them to logs or the repository.

```sh
export NODE_ENV='development'
export HOST='127.0.0.1'
export PORT='8787'
export TRUST_LAYER_DB_FILE='trust-layer-state.json'
export TRUST_LAYER_SECRET='replace-with-a-random-secret'
export TRUST_LAYER_ALLOWED_ORIGINS='["http://127.0.0.1"]'
export TRUST_LAYER_PUBLIC_BASE_URL='http://127.0.0.1:8787'
export TRUST_LAYER_OAUTH_ISSUER='http://127.0.0.1:8787'
export TRUST_LAYER_OAUTH_SIGNING_SECRET='replace-with-a-different-random-secret-of-32-or-more-characters'
export TRUST_LAYER_OAUTH_OPERATOR_USERNAME='local-operator'
export TRUST_LAYER_OAUTH_OPERATOR_PASSWORD='replace-with-a-password-of-12-or-more-characters'

npm start
```

The default `HOST` is `0.0.0.0` and `PORT` is `8787`. The local MCP endpoint is `http://127.0.0.1:8787/mcp`, and the health check is `GET http://127.0.0.1:8787/healthz`. `TRUST_LAYER_PUBLIC_BASE_URL` and `TRUST_LAYER_OAUTH_ISSUER` must share the same origin (scheme, host, port) and must not include a path. In development, explicit loopback HTTP is allowed only for the OAuth URLs, but production requires HTTPS.

### Production Startup (Render + Neon)

In production, specify Neon's connection string as `DATABASE_URL`. With `NODE_ENV=production`, startup is refused if `DATABASE_URL` is absent, and `pg-mem` and the state file are not selected. On Render, `PORT` is injected, so it is usually not set manually.

```sh
npm ci
npm run build
DATABASE_URL='postgresql://...' npm run migrate
npm start
```

`npm run migrate` requires `DATABASE_URL` and applies `001_trust_layer.sql`, `002_runtime_state.sql`, and `003_guidance_version.sql` in order to the real PostgreSQL instance. The PostgreSQL adapter also applies any unapplied migrations in the same order at app startup.

### Environment Variables

| Variable | Requirements and Meaning |
|---|---|
| `NODE_ENV` | Specify `production` in production. When production, `DATABASE_URL` is required and OAuth loopback HTTP is not permitted. |
| `PORT` | Port to listen on. Defaults to `8787` if unspecified. On Render, use the value set by Render. |
| `HOST` | Host to listen on. Defaults to `0.0.0.0` if unspecified. Specify `127.0.0.1` when limited to local. |
| `DATABASE_URL` | When specified, selects the real PostgreSQL adapter. Required when `NODE_ENV=production`. Specify the Neon connection string. |
| `TRUST_LAYER_DB_FILE` | State file for development/testing when `DATABASE_URL` is absent and not in production. Do not use in production. |
| `TRUST_LAYER_SECRET` | Required. Secret value used for Trust Layer's HMAC, etc. Reject if unset, empty, or deprecated development default. Must be different from the OAuth signing secret. |
| `TRUST_LAYER_ALLOWED_ORIGINS` | Required outside test environments. Non-empty allowlist as JSON array or CSV. Rejects `*`, duplicates, and malformed URLs; allows only HTTPS (or loopback HTTP). |
| `TRUST_LAYER_PUBLIC_BASE_URL` | Required. Public origin. Absolute URL without path. HTTPS in production. |
| `TRUST_LAYER_OAUTH_ISSUER` | Required. OAuth issuer. Same origin as `TRUST_LAYER_PUBLIC_BASE_URL`, absolute URL without path. |
| `TRUST_LAYER_OAUTH_SIGNING_SECRET` | Required. 32 characters or more. Must differ from `TRUST_LAYER_SECRET`. |
| `TRUST_LAYER_OAUTH_OPERATOR_USERNAME` | Required. Operator username for OAuth authorization screen. Cannot be blank, up to 160 characters. |
| `TRUST_LAYER_OAUTH_OPERATOR_PASSWORD` | Required. Password for OAuth authorization screen. 12 characters or more. Do not save in logs or Git. |

The code-side `TrustLayerOptions.allowedOrigins` found in older READMEs is not an environment variable name configured by users starting the HTTP server via environment variables. Please specify `TRUST_LAYER_ALLOWED_ORIGINS` at runtime. In addition, the production persistence target is `DATABASE_URL`, not `TRUST_LAYER_DB_FILE`.

### Endpoints and Authentication

- `/mcp`: Streamable HTTP MCP endpoint. Send OAuth access token via `Authorization: Bearer <token>`; requires `trust_layer:tools` scope.
- `/healthz`: Unauthenticated health check. Returns `{"status":"ok"}` on success.
- `/`: Anonymous public lookup Web UI.
- `/stats`: Anonymous public statistics page.
- `GET /api/public/domain-signal?domain=example.org`: Anonymous domain status lookup.
- `GET /api/public/stats`: Anonymous aggregate statistics.
- `POST /api/public/feedback`: Anonymous feedback intake. The current implementation merely returns acceptance, saving no votes, domain statuses, or free-form text.
- `/.well-known/oauth-protected-resource`, `/.well-known/oauth-authorization-server`, `/oauth/register`, `/oauth/authorize`, `/oauth/token`: OAuth metadata, registration, authorization, and token endpoints for MCP connectors.

### The `issue-token` Command

`issue-token` is a CLI that prints a participant token to stdout exactly once, for local/compatibility regression checks of `TrustLayerService`. If the group name is omitted, it defaults to `owner-local`.

```sh
# Development pg-mem + state file
TRUST_LAYER_SECRET='same-local-secret-used-by-the-service' \
TRUST_LAYER_DB_FILE='trust-layer-state.json' \
  npm run issue-token -- --group owner-local

# Using DATABASE_URL (production-equivalent)
DATABASE_URL='postgresql://...' \
TRUST_LAYER_SECRET='same-secret-used-by-the-service' \
  npm run issue-token -- --group owner-test
```

The CLI stores only the token's hash; the plaintext token is printed exactly once. Do not paste the output into logs, environment variables, README, or Git. Since the current HTTP `/mcp` validates OAuth access tokens, tokens issued by this CLI are not used in place of OAuth for official connector configuration.

### Rate Limits and Retention

Public HTTP limits are in-process, using the socket peer address as the source key. They are not a shared limit across multiple Render instances.

- All HTTP requests: 60 requests/60 seconds per source, 300 total/1 second, 32 concurrent. Per-source internal counter capacity is 1024.
- `GET /api/public/domain-signal`: 30 requests/60 seconds per source. Cache misses: 5/1 second overall.
- `GET /api/public/stats`: 30 requests/60 seconds per source.
- `POST /api/public/feedback`: 10 requests/day per source.
- The public API request body limit is 8 KiB; the `/mcp` request body limit is 64 KiB.

Internal limits for authenticated MCP tools are per participant.

- `report_domain_assessments_batch` accepts 1–5 items per call. Input resources: 30 item slots/10 minutes, 300 item slots/24 hours. If invalid input reaches 5 item slots/10 minutes, a 10-minute cooldown applies.
- Assessment acceptance is 10 items/10 minutes, 50 items/24 hours. Batches consume slots proportional to the number of items.
- Contributions from the same participant token × domain × rubric are deduplicated. In the rolling 90-day aggregation, contributions per provenance group are compressed to at most 1.0 equivalent per domain.
- A participant's contribution weight is 0.25 until 7 days have elapsed since issuance AND cumulative contributions reach 10; once both conditions are met, it is 1.

Retention periods are 14 days for manifests, 30 days for assessment receipts, 97 days for aggregate rollups, and 30 days for research ID tombstones. These are deleted by the service's retention process.

## Files and Schemas

- `src/`: TypeScript implementation. `server.ts` handles HTTP/MCP/OAuth routes, `service.ts` handles tool processing and aggregation, `db.ts` handles PostgreSQL/pg-mem adapters, `config.ts` handles secrets, origins, and URL configuration, `oauth.ts` handles OAuth 2.1, and `abuseControls.ts` handles abuse control for public HTTP.
- `schemas/`: 14 JSON Schema files. 5 pairs of input/success schemas for 5 MCP tools, `common-defs.json` for common definitions, `common-error.json` for common errors, `manifest-source-entry-v0.3.json` for manifest source entries, and `public-domain-signal-v1.success.json` for public lookup. Schema IDs are under `https://schemas.trust-layer.local/v0.3/`, loaded by `src/schemaCatalog.ts` as an Ajv catalog. Batch MCP inputSchema is permissive for transport envelopes, but formal per-item validation and outer contract validation are enforced by the service/Ajv.
- `migrations/001_trust_layer.sql`: Creates 14 canonical PostgreSQL-compatible tables (participants, sites, aggregates, rollups, receipts, manifests, source decisions, verification, holds, model map, etc.).
- `migrations/002_runtime_state.sql`: Adds 2 tables: `trust_layer_runtime_state` for application state and `trust_layer_oauth_runtime_state` for OAuth state.
- `migrations/003_guidance_version.sql`: Adds an optional `guidance_version` column to `assessment_event_receipts` and `research_manifests`.
- `tests/`: 6 files: `mcp.test.ts`, `oauth.test.ts`, `publicAccess.test.ts`, `schemaCatalog.test.ts`, `service.test.ts`, and `traceability.test.ts`. Verifies MCP/OAuth/public access, schema catalog, migrations/persistence/transactions, rate/dedup/group caps/TTL, and traceability of unimplemented boundaries.
- `public/`: HTML/JavaScript for anonymous public lookup and stats pages; `data/source-routes.json`: definitions of official information ingress points verified by the operators.

Production migration application and state persistence use a real PostgreSQL instance via `DATABASE_URL`. `TRUST_LAYER_DB_FILE` is for development and testing to bridge `pg-mem` restarts, and is not a substitute for a production database.