privacy
privacy
k-anonymity enforcement with utility measurement, built to run entirely inside
the boundary that holds the regulated data. There is no runtime network access:
records are loaded, generalized, and released in-process, and a test
(tests/airgap.test.ts) fails the build if any source file imports a network or
child-process module.
What it does
Given a table of records and a description of which columns are quasi-identifiers
(age, ZIP, gender — the fields that, combined, re-identify a person), it produces
a release where every combination of quasi-identifier values occurs at least k
times. It does this by full-domain generalization (coarsening values along a
taxonomy or into numeric bins) plus suppression of the records that cannot be
protected any other way. It then reports what that cost in utility.
Architecture
The core is a pure library with no I/O:
generalization.tsturns a raw quasi-identifier value into a coarser label at a chosen level (numeric bins or a categorical taxonomy).kanonymity.tsruns a greedy full-domain search over those levels, suppressing the residual small groups, and provides an independentisKAnonymousaudit.utility.tsscores what enforcement cost (suppression, information loss, discernibility, class size).
Around that core sits the server: tools.ts validates untrusted JSON and applies
resource limits, mcp/server.ts dispatches JSON-RPC, and index.ts frames it
over stdio. Any model-assisted step goes through the PrivacyAdvisor interface,
whose only in-tree implementation is a deterministic local stub — so the whole
system runs offline and, by construction, never sends data off the host. The
decisions behind this shape are recorded in docs/adr/.
Requirements
Node 22.6 or newer (the code is TypeScript executed directly via Node's type stripping; there is no build step).
Install and test
npm ci
npm testnpm test runs the suite with node --test; no API key or network is needed.
Usage
import { enforce, isKAnonymous } from './src/kanonymity.ts';
import { NumericGeneralizer, CategoricalGeneralizer } from './src/generalization.ts';
import { utilityReport } from './src/utility.ts';
const records = [
{ age: 21, zip: 'A1', diagnosis: 'flu' },
{ age: 22, zip: 'A2', diagnosis: 'cold' },
// ...
];
const quasiIdentifiers = [
new NumericGeneralizer('age', 20, 60, 5),
new CategoricalGeneralizer('zip', {
A1: ['A1', 'A', '*'],
A2: ['A2', 'A', '*'],
}),
];
const result = enforce(records, { k: 2, quasiIdentifiers, maxSuppression: 0.05 });
result.satisfied; // true if k met within the suppression ceiling
result.released; // generalized records safe to publish
result.suppressedCount; // records dropped to protect the rest
isKAnonymous(result.released, ['age', 'zip'], 2); // independent audit check
utilityReport(result, quasiIdentifiers); // suppression, info loss, discernibilityField roles without touching the data
StubAdvisor classifies columns into roles (identifier, quasi-identifier,
sensitive, other) from their names only — never their values — so schema
review can happen without exposing cells. It is the deterministic, offline
implementation of the PrivacyAdvisor interface in src/providers/base.ts; a
networked model could replace it, but only behind that same interface and only
by explicit choice.
import { StubAdvisor } from './src/providers/stub.ts';
await new StubAdvisor().suggestRoles(['patient_id', 'age', 'zip', 'diagnosis']);How enforcement chooses levels
The search is greedy. Starting from raw values it repeatedly raises the
generalization level of whichever quasi-identifier most reduces suppression,
breaking ties toward lower information loss, until suppression falls within the
configured ceiling or every field is fully generalized. Greedy full-domain
recoding is fast and produces auditable releases (one level per column), but it
is not guaranteed optimal — a lattice search such as Incognito would find the
minimal generalization at higher cost. That tradeoff is deferred; see the TODO
in src/kanonymity.ts.
Running as an MCP server
The entry point is a Model Context Protocol server speaking JSON-RPC 2.0 over stdio. It has no runtime dependencies and never opens a socket, so it runs where the data lives.
node src/index.tsIt exposes three tools:
classify_fields— assign privacy roles to column names (names only)enforce_kanonymity— generalize/suppress records to meetk, with a utility reportcheck_kanonymity— audit an existing release
Example exchange (one JSON object per line on stdin, responses on stdout):
{"jsonrpc":"2.0","id":1,"method":"initialize"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"enforce_kanonymity","arguments":{"k":2,"records":[{"age":21,"city":"London"},{"age":22,"city":"London"}],"quasiIdentifiers":[{"field":"age","kind":"numeric","min":20,"max":60,"step":5},{"field":"city","kind":"categorical","taxonomy":{"London":["London","*"]}}]}}}Bad input (malformed JSON, unknown tool, oversized request) is reported without
taking the stream down: caller mistakes come back as JSON-RPC errors or
isError tool results, and only genuinely unexpected faults return
-32603. Logs are structured JSON on stderr — stdout carries the protocol only.
Configuration
Set via environment variables; all are optional.
Variable | Default | Meaning |
|
|
|
|
| reject requests with more rows |
|
| cap quasi-identifier columns |
|
| cap categorical taxonomy size |
The limits exist so one oversized request cannot exhaust memory on the host that holds the regulated data. Invalid values fail fast at startup.
Known limitations
k-anonymity alone does not defend against attribute disclosure: if every member of an equivalence class shares the same sensitive value, k-anonymity holds yet the value leaks. l-diversity or t-closeness would address this and are not implemented.
The generalization search is greedy, not optimal (see ADR 0002). It can pick a higher level than a full lattice search would.
Generalizers require explicit bounds/taxonomies; they are not inferred from the data. This is deliberate — inferring bins from values would couple the metadata step to the data it must stay clear of — but it means the caller supplies them.
Enforcement holds the dataset in memory.
PRIVACY_MAX_RECORDSbounds this; there is no streaming or on-disk path for datasets larger than RAM.The
StubAdvisorclassifies by English field-name substrings; non-English or opaque schemas fall through toother.
Layout
src/kanonymity.ts— enforcement and the independentisKAnonymouschecksrc/generalization.ts— numeric and categorical generalizerssrc/utility.ts— suppression rate, information loss, discernibilitysrc/providers/— the advisor interface and its offline stubsrc/mcp/server.ts— JSON-RPC 2.0 message dispatchsrc/tools.ts— input validation, resource limits, tool wiringsrc/config.ts,src/logger.ts— configuration and structured loggingsrc/index.ts— stdio entry point (runServer)tests/— behaviour tests plus the air-gap guard
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/J-X0/ferryyard'
If you have feedback or need assistance with the MCP directory API, please join our Discord server