@stll/anonymize-mcp 0.0.1-placeholder.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ https://www.apache.org/licenses/LICENSE-2.0
4
+
5
+ Copyright 2026 stella labs, s.r.o.
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ https://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # `@stll/anonymize-mcp`
2
+
3
+ Local, path-only MCP tools for stella anonymization. The server uses stdio only,
4
+ performs no network I/O, accepts no document text in tool arguments, and never
5
+ returns document text or plaintext session mappings.
6
+
7
+ Requires Node.js 20 or newer. This requirement applies to the MCP package only;
8
+ its native advisory-lock loader does not support Node.js 18.
9
+
10
+ ```json
11
+ {
12
+ "mcpServers": {
13
+ "stella-anonymize": {
14
+ "command": "npx",
15
+ "args": [
16
+ "-y",
17
+ "@stll/anonymize-mcp",
18
+ "--root",
19
+ "/absolute/path/to/workspace",
20
+ "--pdftoppm",
21
+ "/absolute/path/to/pdftoppm",
22
+ "--tesseract",
23
+ "/absolute/path/to/tesseract"
24
+ ]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ Repeat `--root` to allow more than one directory. Inputs and outputs must be
31
+ absolute paths within those roots. Outputs must be explicit new paths; the
32
+ server refuses overwrites and writes with owner-only permissions. Text must be
33
+ valid UTF-8; text and DOCX inputs are limited to 64 MiB. A server process holds
34
+ at most 256 in-memory sessions.
35
+
36
+ Tools:
37
+
38
+ - `capabilities` (public manifest and MCP metadata only)
39
+ - `anonymize_text_file`
40
+ - `anonymize_text_file_with_external_detections`
41
+ - `restore_text_file`
42
+ - `anonymize_docx_file`
43
+ - `anonymize_pdf_file`
44
+ - `restore_docx_file`
45
+ - `inspect_docx_file`
46
+
47
+ `anonymize_pdf_file` accepts only absolute input/output PDF paths inside the
48
+ configured roots plus bounded raster options and one explicit Tesseract language
49
+ pack. Executable paths are server startup configuration and never tool input.
50
+ The tool renders and OCRs locally, then publishes a fresh image-only PDF; it
51
+ never overlays or copies source PDF objects. Its response contains aggregate
52
+ page/detection/region counts and structural verification only—never document
53
+ text, spans, mappings, or a claim of perfect OCR/detector recall. The certificate
54
+ always reports `piiCleanGuaranteed=false`.
55
+
56
+ By default, sessions live only in the MCP server process. Restoration therefore
57
+ requires the same `sessionId` and the same running process that performed
58
+ anonymization. Stopping the server discards mappings.
59
+
60
+ ## External detection sidecars
61
+
62
+ `anonymize_text_file_with_external_detections` merges native detections with a
63
+ provider-neutral `ExternalDetectionBatch` v1 JSON sidecar. The tool accepts only
64
+ an input `.txt` path, sidecar `.json` path, new output `.txt` path, `sessionId`,
65
+ and the same optional in-memory `language`; it never accepts or returns document
66
+ content, spans, mappings, or provider payloads. Both inputs must be inside the
67
+ configured roots. Text is capped at 64 MiB and the sidecar at 16 MiB.
68
+
69
+ A model-neutral local workflow is:
70
+
71
+ 1. An application-owned fake or real provider reads the local text file.
72
+ 2. It writes a closed-schema v1 sidecar containing the exact document SHA-256,
73
+ one declared offset unit, explicit label mappings, and non-PII provenance
74
+ IDs.
75
+ 3. The MCP client calls the external-detection tool with paths only.
76
+ 4. stella validates the digest/schema/offset boundaries, runs native detection,
77
+ merges both sources through the normal session pipeline, and publishes only
78
+ the anonymized file plus aggregate counts.
79
+
80
+ This package includes no GLiNER dependency or model runner. A fake provider is
81
+ enough for deterministic integration tests; any real provider remains outside
82
+ the MCP trust boundary. Stale digests, unknown fields, invalid boundaries,
83
+ unsafe paths, and oversized sidecars fail before output publication. In durable
84
+ mode the tool uses the full package and rejects `language`, exactly like the
85
+ ordinary text tool.
86
+
87
+ Failures from this tool cross the MCP boundary only as fixed audit-safe error
88
+ codes and messages. Parser, provider, label, document, path, native-plan, and
89
+ storage details are never copied into tool errors or logs.
90
+
91
+ ## Encrypted durable sessions
92
+
93
+ Durable sessions are opt-in. Supply both an existing absolute private session
94
+ directory and an existing absolute private key file; supplying only one fails
95
+ startup:
96
+
97
+ ```json
98
+ {
99
+ "mcpServers": {
100
+ "stella-anonymize": {
101
+ "command": "npx",
102
+ "args": [
103
+ "-y",
104
+ "@stll/anonymize-mcp",
105
+ "--root",
106
+ "/absolute/path/to/workspace",
107
+ "--session-dir",
108
+ "/absolute/private/path/to/sessions",
109
+ "--key-file",
110
+ "/absolute/private/path/to/session.key"
111
+ ]
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ The key file format is exactly 32 raw bytes; text, hexadecimal, base64, and
118
+ password-derived keys are not accepted. The key file must be a regular,
119
+ owner-owned file with no group or other permissions. The session directory must
120
+ already exist, be owner-owned, grant no group or other permissions, and contain
121
+ no symbolic-link path components. Generate and protect the key outside the MCP
122
+ server; losing it makes archives unrecoverable, while disclosure permits anyone
123
+ with the archives to recover mappings.
124
+
125
+ Durable mode currently supports macOS and Linux only. Startup fails closed on
126
+ other platforms because the server requires POSIX owner identities,
127
+ `O_NOFOLLOW`, directory handles and `fsync`, plus process-lifetime advisory file
128
+ locks. One server exclusively locks the whole session directory; a second
129
+ server fails startup, and the operating system releases the lock if the holder
130
+ exits or crashes. The persistent `.stella-session.lock` file contains no session
131
+ metadata and is not itself proof that a live process holds the lock.
132
+
133
+ The server stores only bounded, authenticated encrypted session archives. It
134
+ never writes plaintext mappings or includes key material in tools, results, or
135
+ logs. Archive filenames are SHA-256 hashes of validated session IDs. Writes are
136
+ owner-only and atomically replace the preceding archive. The archive is made
137
+ durable before its corresponding output is published. If output publication
138
+ fails, the server rolls both memory and durable storage back to the preceding
139
+ successful state, or deletes a newly created session. If rollback storage itself
140
+ fails, the in-memory copy is discarded and later work must reload and
141
+ authenticate whichever atomic archive remains. At most 256 archives and 256 MiB
142
+ of archive data are accepted. Restore authenticates the expected session ID and
143
+ evaluates lifecycle expiry at the current time. A wrong key, tampered archive,
144
+ expired session, unsafe path, partial write, or permission change fails closed.
145
+
146
+ The encrypted core archive intentionally contains session state, not MCP
147
+ pipeline-selection metadata. To keep pipeline identity stable across restarts,
148
+ durable sessions always use the full all-language package and reject a
149
+ `language` argument. In-memory sessions retain the existing language-scoped
150
+ behavior. This avoids unauthenticated sidecar metadata and never guesses which
151
+ language package created an archive.
152
+
153
+ Transport closure, `SIGINT`, and `SIGTERM` stop new work, drain active
154
+ operations, close the durable store, release its advisory lock, and zero the
155
+ in-memory key before the process exits.
156
+
157
+ Tool results contain only aggregate, audit-safe counts and statuses. The
158
+ read-only `capabilities` tool returns `CAPABILITY_MANIFEST`, the native runtime
159
+ version, tool and format lists, stdio transport metadata, and either `memory` or
160
+ `durable-encrypted` session mode; it never returns document or session data.
@@ -0,0 +1,172 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod/v4";
3
+ //#region src/durable-sessions.d.ts
4
+ declare const SESSION_ARCHIVE_KEY_BYTES = 32;
5
+ declare const SESSION_ARCHIVE_MAX_BYTES: number;
6
+ declare const SESSION_ARCHIVE_MAX_COUNT = 256;
7
+ declare const SESSION_ARCHIVE_TOTAL_MAX_BYTES: number;
8
+ declare const DURABLE_SESSION_FAULT_POINTS: {
9
+ readonly beforeDirectoryFsync: "before-directory-fsync";
10
+ readonly beforeRename: "before-rename";
11
+ readonly beforeStagingFsync: "before-staging-fsync";
12
+ readonly beforeStagingWrite: "before-staging-write";
13
+ };
14
+ type DurableSessionFaultPoint = (typeof DURABLE_SESSION_FAULT_POINTS)[keyof typeof DURABLE_SESSION_FAULT_POINTS];
15
+ type DurableSessionStoreOptions = {
16
+ faultInjector?: (point: DurableSessionFaultPoint) => Promise<void> | void;
17
+ keyFile: string;
18
+ sessionDirectory: string;
19
+ };
20
+ type StoredSessionArchive = {
21
+ bytes: Uint8Array;
22
+ };
23
+ type EncryptableSession = {
24
+ toEncryptedArchiveAt(key: Uint8Array, observedAtEpochSeconds: number): Uint8Array;
25
+ };
26
+ type EncryptedSessionRestorer<Session> = {
27
+ restoreEncryptedRedactionSession(options: {
28
+ archive: Uint8Array;
29
+ expectedSessionId: string;
30
+ key: Uint8Array;
31
+ observedAtEpochSeconds: number;
32
+ }): Session;
33
+ };
34
+ type RestoreStoredSessionOptions<Session> = {
35
+ archive: Uint8Array;
36
+ expectedSessionId: string;
37
+ observedAtEpochSeconds: number;
38
+ restorer: EncryptedSessionRestorer<Session>;
39
+ };
40
+ declare class DurableSessionStore {
41
+ #private;
42
+ private constructor();
43
+ static create({ keyFile, sessionDirectory, faultInjector }: DurableSessionStoreOptions): Promise<DurableSessionStore>;
44
+ seal(session: EncryptableSession, observedAtEpochSeconds: number): Uint8Array;
45
+ restore<Session>({ archive, expectedSessionId, observedAtEpochSeconds, restorer }: RestoreStoredSessionOptions<Session>): Session;
46
+ close(): Promise<void>;
47
+ load(sessionId: string): Promise<StoredSessionArchive | undefined>;
48
+ save(sessionId: string, archive: Uint8Array): Promise<void>;
49
+ delete(sessionId: string): Promise<void>;
50
+ }
51
+ //#endregion
52
+ //#region src/local.d.ts
53
+ declare const MCP_SESSION_MODES: {
54
+ readonly durableEncrypted: "durable-encrypted";
55
+ readonly memory: "memory";
56
+ };
57
+ type McpSessionMode = (typeof MCP_SESSION_MODES)[keyof typeof MCP_SESSION_MODES];
58
+ type AuditSafeResult = {
59
+ operation: "anonymize" | "inspect" | "restore";
60
+ format: "docx" | "pdf" | "text";
61
+ outputCreated: boolean;
62
+ sessionId?: string;
63
+ entityCount?: number;
64
+ blockCount?: number;
65
+ rewrittenBlockCount?: number;
66
+ restoredPlaceholderCount?: number;
67
+ coverageStatus?: "full" | "partial";
68
+ externalDetectionBatchStatus?: "accepted";
69
+ externalDetectionCount?: number;
70
+ retainedExternalDetectionCount?: number;
71
+ pageCount?: number;
72
+ mappedRegionCount?: number;
73
+ structurePixelRewriteVerified?: true;
74
+ piiCleanGuaranteed?: false;
75
+ };
76
+ type LocalAnonymizeServiceFaults = {
77
+ beforeOutputPublish?: () => void;
78
+ };
79
+ type ReadInputOptions = {
80
+ path: string;
81
+ extension: ".docx" | ".json" | ".pdf" | ".txt";
82
+ maximumBytes: number;
83
+ label: "DOCX" | "External detection batch" | "PDF" | "Text";
84
+ };
85
+ type ScopedInput = {
86
+ bytes: Uint8Array;
87
+ path: string;
88
+ };
89
+ type DirectoryIdentity = {
90
+ dev: number;
91
+ ino: number;
92
+ path: string;
93
+ };
94
+ declare class ScopedOutput {
95
+ readonly parent: DirectoryIdentity;
96
+ readonly path: string;
97
+ constructor(path: string, parent: DirectoryIdentity);
98
+ write(bytes: Uint8Array | string): Promise<void>;
99
+ }
100
+ declare class PathScope {
101
+ #private;
102
+ private constructor();
103
+ static create(roots: readonly string[]): Promise<PathScope>;
104
+ readInput({ path, extension, maximumBytes, label }: ReadInputOptions): Promise<ScopedInput>;
105
+ output(path: string, extension: ".docx" | ".pdf" | ".txt"): Promise<ScopedOutput>;
106
+ }
107
+ declare const textInput: z.ZodObject<{
108
+ inputPath: z.ZodString;
109
+ outputPath: z.ZodString;
110
+ sessionId: z.ZodString;
111
+ language: z.ZodOptional<z.ZodString>;
112
+ }, z.core.$strip>;
113
+ declare const externalDetectionTextInput: z.ZodObject<{
114
+ inputPath: z.ZodString;
115
+ outputPath: z.ZodString;
116
+ sessionId: z.ZodString;
117
+ language: z.ZodOptional<z.ZodString>;
118
+ detectionBatchPath: z.ZodString;
119
+ }, z.core.$strip>;
120
+ declare const restoreInput: z.ZodObject<{
121
+ inputPath: z.ZodString;
122
+ outputPath: z.ZodString;
123
+ sessionId: z.ZodString;
124
+ }, z.core.$strip>;
125
+ declare const docxRestoreInput: z.ZodObject<{
126
+ inputPath: z.ZodString;
127
+ outputPath: z.ZodString;
128
+ sessionId: z.ZodString;
129
+ allowPartialCoverage: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
130
+ }, z.core.$strip>;
131
+ declare const docxInput: z.ZodObject<{
132
+ inputPath: z.ZodString;
133
+ outputPath: z.ZodString;
134
+ sessionId: z.ZodString;
135
+ language: z.ZodOptional<z.ZodString>;
136
+ allowPartialCoverage: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
137
+ }, z.core.$strip>;
138
+ declare const pdfInput: z.ZodObject<{
139
+ inputPath: z.ZodString;
140
+ outputPath: z.ZodString;
141
+ ocrLanguage: z.ZodString;
142
+ detectionLanguage: z.ZodOptional<z.ZodString>;
143
+ dpi: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
144
+ timeoutMs: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
145
+ fillRgb: z.ZodDefault<z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>>;
146
+ }, z.core.$strip>;
147
+ type LocalPdfProviderConfiguration = {
148
+ pdftoppmPath?: string | undefined;
149
+ tesseractPath?: string | undefined;
150
+ };
151
+ type LocalAnonymizeServiceOptions = {
152
+ durableSessions?: DurableSessionStore | undefined;
153
+ faults?: LocalAnonymizeServiceFaults | undefined;
154
+ pdfProvider?: LocalPdfProviderConfiguration | undefined;
155
+ };
156
+ declare class LocalAnonymizeService {
157
+ #private;
158
+ constructor(scope: PathScope, options?: LocalAnonymizeServiceOptions);
159
+ get sessionMode(): McpSessionMode;
160
+ close(): Promise<void>;
161
+ anonymizeText(input: z.infer<typeof textInput>): Promise<AuditSafeResult>;
162
+ anonymizePdf(input: z.infer<typeof pdfInput>): Promise<AuditSafeResult>;
163
+ restoreText(input: z.infer<typeof restoreInput>): Promise<AuditSafeResult>;
164
+ anonymizeTextWithExternalDetections(input: z.infer<typeof externalDetectionTextInput>): Promise<AuditSafeResult>;
165
+ anonymizeDocx(input: z.infer<typeof docxInput>): Promise<AuditSafeResult>;
166
+ restoreDocx(input: z.infer<typeof docxRestoreInput>): Promise<AuditSafeResult>;
167
+ inspectDocx(inputPath: string): Promise<AuditSafeResult>;
168
+ }
169
+ declare const createAnonymizeMcpServer: (service: LocalAnonymizeService) => McpServer;
170
+ //#endregion
171
+ export { type AuditSafeResult, DURABLE_SESSION_FAULT_POINTS, type DurableSessionFaultPoint, DurableSessionStore, type DurableSessionStoreOptions, type EncryptableSession, type EncryptedSessionRestorer, LocalAnonymizeService, type LocalAnonymizeServiceFaults, type LocalAnonymizeServiceOptions, type LocalPdfProviderConfiguration, MCP_SESSION_MODES, type McpSessionMode, PathScope, type RestoreStoredSessionOptions, SESSION_ARCHIVE_KEY_BYTES, SESSION_ARCHIVE_MAX_BYTES, SESSION_ARCHIVE_MAX_COUNT, SESSION_ARCHIVE_TOTAL_MAX_BYTES, type StoredSessionArchive, createAnonymizeMcpServer };
172
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { a as DURABLE_SESSION_FAULT_POINTS, c as SESSION_ARCHIVE_MAX_BYTES, i as createAnonymizeMcpServer, l as SESSION_ARCHIVE_MAX_COUNT, n as MCP_SESSION_MODES, o as DurableSessionStore, r as PathScope, s as SESSION_ARCHIVE_KEY_BYTES, t as LocalAnonymizeService, u as SESSION_ARCHIVE_TOTAL_MAX_BYTES } from "./local.mjs";
2
+ export { DURABLE_SESSION_FAULT_POINTS, DurableSessionStore, LocalAnonymizeService, MCP_SESSION_MODES, PathScope, SESSION_ARCHIVE_KEY_BYTES, SESSION_ARCHIVE_MAX_BYTES, SESSION_ARCHIVE_MAX_COUNT, SESSION_ARCHIVE_TOTAL_MAX_BYTES, createAnonymizeMcpServer };