diff --git a/docs/llms.txt b/docs/llms.txt index f1385a52..eee86b91 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -19,9 +19,12 @@ Power Systems Wiki compares definitions from papers, standards, and industry rep - [OpenAPI specification](https://ps-wiki.ning.guru/api/openapi.yaml): REST API contract. - [REST API documentation](https://ps-wiki.ning.guru/rest-api/): Endpoints and examples. - [MCP server documentation](https://ps-wiki.ning.guru/mcp/): Tools for AI assistants and MCP clients. +- [Privacy policy](https://ps-wiki.ning.guru/privacy/): Request telemetry and data handling. +- [Service terms](https://ps-wiki.ning.guru/terms/): Read-only API and MCP usage terms. ## Attribution and interpretation - Preserve links or citations to the original sources when summarizing a definition. - Distinguish between definitions from different organizations instead of merging them into a single authoritative definition. +- The remote MCP server is public, read-only, and available at `https://mcp.ps-wiki.ning.guru/mcp` over Streamable HTTP. - The wiki content is available under [CC BY-NC 4.0](https://github.com/ps-wiki/ps-wiki.github.io/blob/main/LICENSE). diff --git a/docs/mcp.md b/docs/mcp.md index d4762e41..1b6f79c2 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -111,25 +111,76 @@ standard local proxy: } ``` -In ChatGPT or another client with remote MCP/connectors support, add the server -URL `https://mcp.ps-wiki.ning.guru/mcp` in that client’s MCP/connector settings. UI -availability and approval controls depend on the account or workspace. For the -OpenAI Responses API, the protocol-level shape is: +### ChatGPT and the OpenAI API + +For a current ChatGPT workspace, an administrator or owner can create a custom +MCP app in Developer Mode, scan the tools, and publish the app to the +workspace. Full MCP app support is currently available on ChatGPT Business and +Enterprise/Edu web workspaces; availability and permissions can change as the +feature evolves. Use the [official ChatGPT MCP app +instructions](https://help.openai.com/en/articles/12584461-developer-mode-and-mcp-apps-in-chatgpt) +for the current UI flow. + +For the OpenAI Responses API, the protocol-level shape is: ```python response = client.responses.create( - model="gpt-5", + model="gpt-6-astra", input="Explain voltage stability using PS-Wiki.", tools=[{ "type": "mcp", "server_label": "pswiki", + "server_description": "Read-only power-systems terminology from PS-Wiki.", "server_url": "https://mcp.ps-wiki.ning.guru/mcp", - "allowed_tools": {"read_only": True}, + "allowed_tools": [ + "search_terms", + "get_term", + "get_related_terms", + "list_tags", + "get_terms_by_tag", + ], "require_approval": "never", }], ) ``` +The current OpenAI API uses an array of tool names for `allowed_tools`. Replace +the example model with one currently available to your account. `never` is +appropriate here because all five PS-Wiki tools are read-only; use approvals +if write-capable tools are added later. + +### Claude + +Claude users can add the public endpoint as a custom connector: + +1. On an individual Pro or Max plan, open **Customize → Connectors**, choose + **Add custom connector**, and enter `https://mcp.ps-wiki.ning.guru/mcp`. +2. On Team or Enterprise, an Owner adds it under **Organization settings → + Connectors → Add → Custom → Web**; members then connect it from + **Customize → Connectors**. +3. Enable the connector for the conversation from the **+** menu. + +See [Claude's current remote MCP connector +instructions](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp). +Claude connects to remote connectors from Anthropic's infrastructure, so the +server must remain publicly reachable. + +### Directory listings + +Direct custom connections do not require a directory listing. A public listing +is a separate, optional distribution step: + +- [OpenAI plugin submission](https://developers.openai.com/plugins/deploy/submission) + accepts a universal public MCP URL, but requires publisher identity + verification, domain verification, and public website, support, privacy, and + terms URLs. +- Anthropic's [Connectors Directory](https://support.anthropic.com/en/articles/11596036-anthropic-mcp-directory-faq) + is curated and requires a review submission; inclusion is not guaranteed. + +PS-Wiki is prepared for these paths, but directory submission still requires +the project owner's account, identity verification, and any reviewer-domain +challenge. + ## Tools ### `search_terms` @@ -243,6 +294,12 @@ milliseconds. Tool arguments, prompts, query text, response bodies, IP addresses, and session identifiers are not logged. A tool that makes multiple REST requests records the bounded list of observed upstream statuses. +The R2 traffic log stores one JSON object per request with the worker, method, +origin/path, status, duration, request ID, and selected Cloudflare location +metadata. Query strings are intentionally omitted so REST search text is not +written to durable traffic logs. See the [privacy policy](privacy.md) and +[service terms](terms.md) for the current data-handling and usage details. + Workers Logs are diagnostic rather than durable usage analytics; retention is limited by the Cloudflare plan. The separate monitoring repository stores aggregated request data for longer-term reporting. @@ -265,3 +322,5 @@ The local stdio server also retains its existing read-only resources: PS-Wiki content is licensed under [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/). The remote MCP layer delegates data retrieval to the [PS-Wiki REST API](rest-api.md). +For data handling and support, see the [privacy policy](privacy.md) and +[service terms](terms.md). diff --git a/docs/privacy.md b/docs/privacy.md new file mode 100644 index 00000000..e768aba3 --- /dev/null +++ b/docs/privacy.md @@ -0,0 +1,69 @@ +--- +title: Privacy Policy +description: How PS-Wiki handles website, REST API, and MCP request data +--- + +# Privacy Policy + +**Last updated: 2026-09-14.** + +This policy describes the request data handled by the Power Systems Wiki +(“PS-Wiki”) website, REST API, and remote MCP server. The service is operated +as an open, read-only reference project. + +## Data handled + +When you visit the site or call an API, Cloudflare may process ordinary network +and security data needed to deliver the service. PS-Wiki's application-level +telemetry is limited as follows: + +- The REST and MCP Workers write a durable R2 traffic record containing the + timestamp, worker name, HTTP method, origin and path, response status, + duration, request ID, and selected Cloudflare location metadata (colo, + country, and ASN when available). +- Query strings are omitted from durable R2 traffic records. This prevents + REST search text and similar parameters from being retained in those logs. +- MCP tool telemetry records the tool name, upstream REST status information, + bounded error classes, and elapsed time. It does not record tool arguments, + prompts, or response bodies. +- The REST Worker may emit short-lived diagnostic logs containing method, path, + country, colo, User-Agent, Referer, and Accept headers. These logs are + controlled by Cloudflare's Workers Logs retention and access controls. + +PS-Wiki does not require an account, does not ask for passwords, and does not +intentionally collect the contents of conversations sent by an AI client. +Please do not include confidential or sensitive information in search terms or +other requests. + +## Purpose and retention + +Telemetry is used to operate the service, diagnose failures, understand broad +usage, and plan capacity. R2 traffic records are retained until the project +owner deletes them or adds a Cloudflare R2 lifecycle rule; the current Worker +configuration does not set an automatic expiration. Cloudflare diagnostic-log +retention may be shorter and depends on the account plan. + +The PS-Wiki application does not use these logs to train an AI model. MCP +clients and model providers may handle prompts and tool results under their +own policies; review the policy of the client you choose. + +## Sharing and service providers + +The Workers, R2 bucket, and diagnostic logging are provided through Cloudflare. +The remote MCP Worker retrieves public wiki data from the PS-Wiki REST API. +PS-Wiki does not sell application telemetry. Data may be processed by these +service providers as necessary to deliver, secure, and operate the service. + +## Your choices and requests + +You can stop sending requests at any time. To ask about a traffic record or +report a privacy concern, open a [GitHub issue](https://github.com/ps-wiki/ps-wiki.github.io/issues) +with the approximate date, worker, and route; do not include private content. +Because traffic records use generated request IDs and do not contain account +identifiers, a request may not be identifiable without sufficient timing and +route information. + +## Changes + +This policy may be updated when the service or its telemetry changes. The +effective date appears at the top of this page. diff --git a/docs/terms.md b/docs/terms.md new file mode 100644 index 00000000..8316cfaf --- /dev/null +++ b/docs/terms.md @@ -0,0 +1,44 @@ +--- +title: Service Terms +description: Terms for using the PS-Wiki REST API and remote MCP server +--- + +# PS-Wiki API and MCP Service Terms + +**Last updated: 2026-09-14.** + +These terms apply to the public, read-only PS-Wiki REST API and remote MCP +server. By using either service, you agree to use it lawfully and in a way +that does not impair the service for others. + +## Service scope + +The API and MCP server provide access to public PS-Wiki terminology data. The +remote MCP endpoint is `https://mcp.ps-wiki.ning.guru/mcp`. It currently exposes +read-only search, retrieval, relationship, and tag-browsing tools. The service +may change, rate-limit, or temporarily become unavailable without notice. + +## Responsible use + +Do not use the service to send confidential information, credentials, personal +data, or material that you are not authorized to disclose. Do not attempt to +bypass access controls, overload the service, or misrepresent PS-Wiki output as +an authoritative standard or professional advice. + +When using definitions in other work, preserve the source citations and +attribution returned by the API or MCP tools. Wiki content is available under +[CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/); those license +terms govern reuse of the content. + +## No warranty + +PS-Wiki is an open reference project. The service and its content are provided +on an “as is” basis. Check the cited primary source, applicable standard, or +professional guidance before relying on a definition for engineering, +operational, safety, legal, or commercial decisions. + +## Contact + +For corrections, service problems, or questions, use the +[GitHub issue tracker](https://github.com/ps-wiki/ps-wiki.github.io/issues). +Privacy matters are described in the [privacy policy](privacy.md). diff --git a/mkdocs.yml b/mkdocs.yml index f7922c2d..708270da 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -58,6 +58,8 @@ nav: - By Tags: tags.md - REST API: rest-api.md - MCP Server: mcp.md + - Privacy: privacy.md + - Terms: terms.md - Development: development.md - Changelog: changelog.md diff --git a/worker/src/mcp.ts b/worker/src/mcp.ts index b43a8b54..5a95b7d3 100644 --- a/worker/src/mcp.ts +++ b/worker/src/mcp.ts @@ -292,7 +292,13 @@ function createServer(env: McpEnv): McpServer { "search_terms", { description: "Search PS-Wiki terminology by keyword, phrase, or concept. Use this to discover candidate terms before requesting a complete definition.", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, + annotations: { + title: "Search PS-Wiki terms", + readOnlyHint: true, + openWorldHint: false, + destructiveHint: false, + idempotentHint: true, + }, inputSchema: { query: z.string().trim().min(1).max(200).describe("Keyword, phrase, or concept to search for."), limit: z.number().int().min(1).max(MAX_SEARCH_LIMIT).optional().default(10).describe("Maximum number of matching term summaries to return; defaults to 10."), @@ -309,7 +315,13 @@ function createServer(env: McpEnv): McpServer { "get_term", { description: "Retrieve the complete PS-Wiki definition for one known term ID, including description, equations, citations, related terms, canonical URL, and attribution.", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, + annotations: { + title: "Get a PS-Wiki term", + readOnlyHint: true, + openWorldHint: false, + destructiveHint: false, + idempotentHint: true, + }, inputSchema: { term_id: z.string().trim().min(1).max(200).describe("Kebab-case PS-Wiki term ID, such as voltage-stability."), }, @@ -322,7 +334,13 @@ function createServer(env: McpEnv): McpServer { "get_related_terms", { description: "Explore concepts related to a known PS-Wiki term. Use depth 1 for direct relationships; depth 2 also includes a bounded set of second-level concepts.", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, + annotations: { + title: "Get related PS-Wiki terms", + readOnlyHint: true, + openWorldHint: false, + destructiveHint: false, + idempotentHint: true, + }, inputSchema: { term_id: z.string().trim().min(1).max(200).describe("Kebab-case PS-Wiki term ID."), depth: z.number().int().min(1).max(2).optional().default(1).describe("Relationship depth: 1 or 2; defaults to 1."), @@ -371,7 +389,13 @@ function createServer(env: McpEnv): McpServer { "list_tags", { description: "List PS-Wiki categories/tags and their usage counts. Use this to browse the taxonomy before filtering terms by a tag.", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, + annotations: { + title: "List PS-Wiki tags", + readOnlyHint: true, + openWorldHint: false, + destructiveHint: false, + idempotentHint: true, + }, inputSchema: {}, }, async () => @@ -386,7 +410,13 @@ function createServer(env: McpEnv): McpServer { "get_terms_by_tag", { description: "Browse PS-Wiki terms in one category/tag. Use this when the user wants a taxonomy-based list rather than a free-text concept search.", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, + annotations: { + title: "List PS-Wiki terms by tag", + readOnlyHint: true, + openWorldHint: false, + destructiveHint: false, + idempotentHint: true, + }, inputSchema: { tag: z.string().trim().min(1).max(100).describe("Case-insensitive PS-Wiki tag, such as stability or control."), }, diff --git a/worker/src/shared/traffic-logger.ts b/worker/src/shared/traffic-logger.ts index 2cfeb2d0..7f2b19a8 100644 --- a/worker/src/shared/traffic-logger.ts +++ b/worker/src/shared/traffic-logger.ts @@ -105,12 +105,14 @@ function writeLogEntry( ): void { const durationMs = Date.now() - startMs; + const requestUrl = request ? new URL(request.url) : undefined; const entry: TrafficLogEntry = { timestamp, worker: workerName, method: request?.method ?? "UNKNOWN", - url: request?.url ?? "", - pathname: request ? new URL(request.url).pathname : "", + // Keep the route for traffic analysis without persisting query text. + url: requestUrl ? `${requestUrl.origin}${requestUrl.pathname}` : "", + pathname: requestUrl?.pathname ?? "", status, durationMs, requestId, diff --git a/worker/tests/mcp.test.ts b/worker/tests/mcp.test.ts index 2b1d0f1e..63836364 100644 --- a/worker/tests/mcp.test.ts +++ b/worker/tests/mcp.test.ts @@ -20,7 +20,16 @@ const context = { type McpReply = { result?: { serverInfo?: { name?: string }; - tools?: Array<{ name: string }>; + tools?: Array<{ + name: string; + annotations?: { + title?: string; + readOnlyHint?: boolean; + openWorldHint?: boolean; + destructiveHint?: boolean; + idempotentHint?: boolean; + }; + }>; isError?: boolean; content?: Array<{ text: string }>; }; @@ -131,6 +140,13 @@ describe("remote MCP Worker", () => { "list_tags", "get_terms_by_tag", ]); + expect(listed.body!.result!.tools!.map((tool) => tool.annotations)).toEqual([ + { title: "Search PS-Wiki terms", readOnlyHint: true, openWorldHint: false, destructiveHint: false, idempotentHint: true }, + { title: "Get a PS-Wiki term", readOnlyHint: true, openWorldHint: false, destructiveHint: false, idempotentHint: true }, + { title: "Get related PS-Wiki terms", readOnlyHint: true, openWorldHint: false, destructiveHint: false, idempotentHint: true }, + { title: "List PS-Wiki tags", readOnlyHint: true, openWorldHint: false, destructiveHint: false, idempotentHint: true }, + { title: "List PS-Wiki terms by tag", readOnlyHint: true, openWorldHint: false, destructiveHint: false, idempotentHint: true }, + ]); }); it("calls search_terms through the REST API", async () => { diff --git a/worker/tests/traffic-logger.test.ts b/worker/tests/traffic-logger.test.ts new file mode 100644 index 00000000..d328ee2d --- /dev/null +++ b/worker/tests/traffic-logger.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it, vi } from "vitest"; + +import { logTrafficWithRequest } from "../src/shared/traffic-logger"; + +describe("traffic logger", () => { + it("stores route metadata without query text and does not delay the response", async () => { + const put = vi.fn(async () => undefined); + const waitUntil = vi.fn(); + const response = await logTrafficWithRequest( + "rest-api", + { TRAFFIC_LOGS: { put } as unknown as R2Bucket }, + { waitUntil }, + new Request("https://api.ps-wiki.ning.guru/v1/terms?query=private-value&limit=5"), + async () => new Response("ok", { status: 200 }), + ); + + expect(response.status).toBe(200); + expect(waitUntil).toHaveBeenCalledOnce(); + expect(put).toHaveBeenCalledOnce(); + const [key, body] = put.mock.calls[0] as unknown as [string, string]; + expect(key).toMatch(/^rest-api\/\d{4}-\d{2}-\d{2}\/.+\.json$/); + expect(JSON.parse(body)).toMatchObject({ + worker: "rest-api", + method: "GET", + url: "https://api.ps-wiki.ning.guru/v1/terms", + pathname: "/v1/terms", + status: 200, + }); + expect(body).not.toContain("private-value"); + }); +});