Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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).
71 changes: 65 additions & 6 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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.
Expand All @@ -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).
69 changes: 69 additions & 0 deletions docs/privacy.md
Original file line number Diff line number Diff line change
@@ -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.
44 changes: 44 additions & 0 deletions docs/terms.md
Original file line number Diff line number Diff line change
@@ -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).
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 35 additions & 5 deletions worker/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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."),
Expand All @@ -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."),
},
Expand All @@ -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."),
Expand Down Expand Up @@ -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 () =>
Expand All @@ -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."),
},
Expand Down
6 changes: 4 additions & 2 deletions worker/src/shared/traffic-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 17 additions & 1 deletion worker/tests/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>;
};
Expand Down Expand Up @@ -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 () => {
Expand Down
31 changes: 31 additions & 0 deletions worker/tests/traffic-logger.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
Loading