openalmanac 0.4.2 → 0.4.4
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/dist/auth.d.ts +1 -0
- package/dist/auth.js +14 -0
- package/dist/instructions.js +3 -3
- package/dist/onboarding-copy.js +1 -1
- package/dist/server.js +6 -4
- package/dist/setup/clients.js +1 -1
- package/dist/tool-registry.js +1 -1
- package/dist/tool-tracking.d.ts +49 -0
- package/dist/tool-tracking.js +108 -0
- package/dist/tools/auth.js +1 -1
- package/dist/tools/pages/index.js +45 -11
- package/dist/tools/topics.js +2 -1
- package/dist/tools/wikis.js +19 -6
- package/package.json +1 -1
- package/dist/openalmanac_mcp-0.3.1-py3-none-any.whl +0 -0
- package/dist/openalmanac_mcp-0.3.1.tar.gz +0 -0
- package/dist/openalmanac_mcp-0.3.2-py3-none-any.whl +0 -0
- package/dist/openalmanac_mcp-0.3.2.tar.gz +0 -0
package/dist/auth.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type AuthStatus = {
|
|
|
14
14
|
};
|
|
15
15
|
export declare function getAuthStatus(): Promise<AuthStatus>;
|
|
16
16
|
export declare function buildAuthHeaders(): Record<string, string>;
|
|
17
|
+
export declare function requireValidApiKey(): Promise<void>;
|
|
17
18
|
export declare function request(method: string, path: string, options?: {
|
|
18
19
|
auth?: boolean;
|
|
19
20
|
params?: Record<string, string | number | boolean>;
|
package/dist/auth.js
CHANGED
|
@@ -59,6 +59,20 @@ export async function getAuthStatus() {
|
|
|
59
59
|
export function buildAuthHeaders() {
|
|
60
60
|
return { Authorization: `Bearer ${requireApiKey()}` };
|
|
61
61
|
}
|
|
62
|
+
export async function requireValidApiKey() {
|
|
63
|
+
const key = requireApiKey();
|
|
64
|
+
const resp = await fetch(`${API_BASE}/api/users/me`, {
|
|
65
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
66
|
+
signal: AbortSignal.timeout(10_000),
|
|
67
|
+
});
|
|
68
|
+
if (resp.ok)
|
|
69
|
+
return;
|
|
70
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
71
|
+
throw new Error(`Authentication failed (${resp.status}). Your API key may be invalid or expired. Run 'login' to re-authenticate.`);
|
|
72
|
+
}
|
|
73
|
+
const text = await resp.text();
|
|
74
|
+
throw new Error(`${resp.status} ${resp.statusText}: ${text}`);
|
|
75
|
+
}
|
|
62
76
|
export async function request(method, path, options = {}) {
|
|
63
77
|
const { auth = false, params, json, body, contentType } = options;
|
|
64
78
|
let url = `${API_BASE}${path}`;
|
package/dist/instructions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const SERVER_INSTRUCTIONS = [
|
|
2
|
-
"
|
|
2
|
+
"Almanac is an open knowledge base — a Wikipedia anyone can read from and write to through an API. Pages are markdown files with YAML frontmatter, [@key] citation markers, and [[wikilinks]]. Content is organized into wikis, each with topics, pages, and navigation.",
|
|
3
3
|
"",
|
|
4
4
|
"## How this should feel",
|
|
5
5
|
"",
|
|
@@ -113,7 +113,7 @@ export const SERVER_INSTRUCTIONS = [
|
|
|
113
113
|
"",
|
|
114
114
|
"## Working across wikis",
|
|
115
115
|
"",
|
|
116
|
-
"
|
|
116
|
+
"Almanac is multi-wiki. Every page lives inside one wiki. Before writing or creating anything, ground yourself:",
|
|
117
117
|
"",
|
|
118
118
|
"- `whoami` → who is the user? Needed to address them and to reason about \"my wikis\".",
|
|
119
119
|
"- `list_wikis` → what wikis exist? Use this BEFORE `create_wiki` so you can suggest contributing to an existing wiki instead of spinning up a parallel one.",
|
|
@@ -136,7 +136,7 @@ export const SERVER_INSTRUCTIONS = [
|
|
|
136
136
|
"",
|
|
137
137
|
"## Technical workflow",
|
|
138
138
|
"",
|
|
139
|
-
"
|
|
139
|
+
"All Almanac MCP tools except `login` and `logout` require login. Login creates a personal API key linked to your user account, so MCP reads, research, and contributions are attributed to you.",
|
|
140
140
|
"",
|
|
141
141
|
"Core flow: login (once) → `whoami` (confirm identity) → `list_wikis` or `search_pages` (what exists?) → `search_web` + `read_webpage` (research) → `new` (scaffold) or `download` (existing) → edit files under ~/.openalmanac/pages/{wiki_slug}/ → `publish`.",
|
|
142
142
|
"",
|
package/dist/onboarding-copy.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// post-login connected page rendered by `login-core.ts`.
|
|
3
3
|
//
|
|
4
4
|
// The example prompt is a starter question on purpose: the goal is to give
|
|
5
|
-
// new users an easy first message that teaches them what
|
|
5
|
+
// new users an easy first message that teaches them what Almanac can do
|
|
6
6
|
// instead of assuming they already know what to ask. If the example prompt
|
|
7
7
|
// changes, both surfaces (`setup.ts` next steps + `login-core.ts` terminal
|
|
8
8
|
// mock) update automatically because they consume this single constant.
|
package/dist/server.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerTopicTools } from "./tools/topics.js";
|
|
|
10
10
|
import { registerUserTools } from "./tools/users.js";
|
|
11
11
|
import { getApiKey } from "./auth.js";
|
|
12
12
|
import { SERVER_INSTRUCTIONS } from "./instructions.js";
|
|
13
|
+
import { installMcpToolTracking } from "./tool-tracking.js";
|
|
13
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
15
16
|
export function createServer() {
|
|
@@ -17,12 +18,12 @@ export function createServer() {
|
|
|
17
18
|
console.error(`
|
|
18
19
|
┌──────────────────────────────────────────────────┐
|
|
19
20
|
│ │
|
|
20
|
-
│ Welcome to
|
|
21
|
+
│ Welcome to Almanac │
|
|
21
22
|
│ │
|
|
22
23
|
│ Try asking your agent: │
|
|
23
24
|
│ │
|
|
24
|
-
│ → "Write an Almanac page about CORS"
|
|
25
|
-
│ → "Improve the Alan Turing page"
|
|
25
|
+
│ → "Write an Almanac page about CORS" │
|
|
26
|
+
│ → "Improve the Alan Turing page" │
|
|
26
27
|
│ │
|
|
27
28
|
│ Docs: openalmanac.org/contribute │
|
|
28
29
|
│ │
|
|
@@ -30,10 +31,11 @@ export function createServer() {
|
|
|
30
31
|
`);
|
|
31
32
|
}
|
|
32
33
|
const server = new FastMCP({
|
|
33
|
-
name: "
|
|
34
|
+
name: "Almanac",
|
|
34
35
|
version: pkg.version,
|
|
35
36
|
instructions: SERVER_INSTRUCTIONS,
|
|
36
37
|
});
|
|
38
|
+
installMcpToolTracking(server, pkg.version);
|
|
37
39
|
registerAuthTools(server);
|
|
38
40
|
registerPageTools(server);
|
|
39
41
|
registerResearchTools(server);
|
package/dist/setup/clients.js
CHANGED
|
@@ -254,7 +254,7 @@ export function applyClientSetup(clients, mode) {
|
|
|
254
254
|
return { configured, alreadyConfigured };
|
|
255
255
|
}
|
|
256
256
|
export function printSetupPlan(clients, options) {
|
|
257
|
-
const heading = options.dryRun ? "Dry run" : "
|
|
257
|
+
const heading = options.dryRun ? "Dry run" : "Almanac MCP setup";
|
|
258
258
|
process.stdout.write(`${heading}\n\n`);
|
|
259
259
|
if (clients.length === 0) {
|
|
260
260
|
process.stdout.write("No supported clients detected. Use --client <name> to force a target or --print to inspect supported snippets.\n");
|
package/dist/tool-registry.js
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { FastMCP } from "fastmcp";
|
|
2
|
+
type ToolGroup = "read" | "write" | "research" | "wiki_admin" | "account";
|
|
3
|
+
type EntityType = "account" | "image" | "page" | "topic" | "web" | "wiki";
|
|
4
|
+
type MutationScope = "none" | "local" | "backend" | "backend_dry_run";
|
|
5
|
+
type IntentDomain = "auth" | "page_read" | "page_write" | "search" | "topic_management" | "web_research" | "wiki_management";
|
|
6
|
+
export interface McpToolCallEvent {
|
|
7
|
+
tool_name: string;
|
|
8
|
+
tool_group: ToolGroup;
|
|
9
|
+
intent_domain: IntentDomain;
|
|
10
|
+
operation: string;
|
|
11
|
+
entity_type: EntityType;
|
|
12
|
+
mutation_scope: MutationScope;
|
|
13
|
+
success: boolean;
|
|
14
|
+
duration_ms: number;
|
|
15
|
+
mcp_version: string;
|
|
16
|
+
error_type?: string;
|
|
17
|
+
wiki_slug?: string;
|
|
18
|
+
dry_run?: boolean;
|
|
19
|
+
requested_count?: number;
|
|
20
|
+
result_count?: number;
|
|
21
|
+
result_created_count?: number;
|
|
22
|
+
result_updated_count?: number;
|
|
23
|
+
result_renamed_count?: number;
|
|
24
|
+
result_unchanged_count?: number;
|
|
25
|
+
result_error_count?: number;
|
|
26
|
+
result_skipped_count?: number;
|
|
27
|
+
result_stub_created_count?: number;
|
|
28
|
+
planned_created_count?: number;
|
|
29
|
+
planned_updated_count?: number;
|
|
30
|
+
planned_renamed_count?: number;
|
|
31
|
+
planned_error_count?: number;
|
|
32
|
+
}
|
|
33
|
+
interface McpToolDefinition {
|
|
34
|
+
name: string;
|
|
35
|
+
execute?: (...args: any[]) => unknown;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
interface ToolTrackingDeps {
|
|
39
|
+
mcpVersion: string;
|
|
40
|
+
validateAuth?: () => Promise<void>;
|
|
41
|
+
trackToolCall?: (event: McpToolCallEvent) => Promise<void>;
|
|
42
|
+
now?: () => number;
|
|
43
|
+
}
|
|
44
|
+
export type McpTrackingDetails = Partial<Omit<McpToolCallEvent, "tool_name" | "tool_group" | "intent_domain" | "success" | "duration_ms" | "mcp_version" | "error_type">>;
|
|
45
|
+
export declare function setMcpTrackingDetails(details: McpTrackingDetails): void;
|
|
46
|
+
export declare function postMcpToolCall(event: McpToolCallEvent): Promise<void>;
|
|
47
|
+
export declare function wrapMcpToolDefinition<T extends McpToolDefinition>(definition: T, deps: ToolTrackingDeps): T;
|
|
48
|
+
export declare function installMcpToolTracking(server: FastMCP, mcpVersion: string): FastMCP;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { request, requireValidApiKey } from "./auth.js";
|
|
3
|
+
const AUTH_EXEMPT_TOOLS = new Set(["login", "logout"]);
|
|
4
|
+
const trackingDetailsStorage = new AsyncLocalStorage();
|
|
5
|
+
export function setMcpTrackingDetails(details) {
|
|
6
|
+
const store = trackingDetailsStorage.getStore();
|
|
7
|
+
if (store) {
|
|
8
|
+
store.details = { ...store.details, ...details };
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const TOOL_METADATA = {
|
|
12
|
+
search_pages: { tool_group: "read", intent_domain: "search", operation: "page_search", entity_type: "page", mutation_scope: "none" },
|
|
13
|
+
search_topics: { tool_group: "read", intent_domain: "search", operation: "topic_search", entity_type: "topic", mutation_scope: "none" },
|
|
14
|
+
list_pages: { tool_group: "read", intent_domain: "page_read", operation: "page_list", entity_type: "page", mutation_scope: "none" },
|
|
15
|
+
list_topics: { tool_group: "read", intent_domain: "page_read", operation: "topic_list", entity_type: "topic", mutation_scope: "none" },
|
|
16
|
+
list_wikis: { tool_group: "read", intent_domain: "wiki_management", operation: "wiki_list", entity_type: "wiki", mutation_scope: "none" },
|
|
17
|
+
download: { tool_group: "read", intent_domain: "page_read", operation: "page_download", entity_type: "page", mutation_scope: "local" },
|
|
18
|
+
read_page: { tool_group: "read", intent_domain: "page_read", operation: "page_read", entity_type: "page", mutation_scope: "none" },
|
|
19
|
+
search_web: { tool_group: "research", intent_domain: "web_research", operation: "web_search", entity_type: "web", mutation_scope: "none" },
|
|
20
|
+
read_webpage: { tool_group: "research", intent_domain: "web_research", operation: "webpage_read", entity_type: "web", mutation_scope: "none" },
|
|
21
|
+
search_images: { tool_group: "research", intent_domain: "web_research", operation: "image_search", entity_type: "image", mutation_scope: "none" },
|
|
22
|
+
view_images: { tool_group: "research", intent_domain: "web_research", operation: "image_view", entity_type: "image", mutation_scope: "none" },
|
|
23
|
+
new: { tool_group: "write", intent_domain: "page_write", operation: "page_scaffold", entity_type: "page", mutation_scope: "local" },
|
|
24
|
+
publish: { tool_group: "write", intent_domain: "page_write", operation: "page_publish", entity_type: "page", mutation_scope: "backend" },
|
|
25
|
+
delete_pages: { tool_group: "write", intent_domain: "page_write", operation: "page_delete", entity_type: "page", mutation_scope: "backend" },
|
|
26
|
+
create_topics: { tool_group: "write", intent_domain: "topic_management", operation: "topic_create", entity_type: "topic", mutation_scope: "backend" },
|
|
27
|
+
update_topic: { tool_group: "write", intent_domain: "topic_management", operation: "topic_update", entity_type: "topic", mutation_scope: "backend" },
|
|
28
|
+
create_wiki: { tool_group: "wiki_admin", intent_domain: "wiki_management", operation: "wiki_create", entity_type: "wiki", mutation_scope: "backend" },
|
|
29
|
+
get_wiki_settings: { tool_group: "wiki_admin", intent_domain: "wiki_management", operation: "wiki_settings_read", entity_type: "wiki", mutation_scope: "none" },
|
|
30
|
+
update_wiki_settings: { tool_group: "wiki_admin", intent_domain: "wiki_management", operation: "wiki_settings_update", entity_type: "wiki", mutation_scope: "backend" },
|
|
31
|
+
join_wiki: { tool_group: "wiki_admin", intent_domain: "wiki_management", operation: "wiki_join", entity_type: "wiki", mutation_scope: "backend" },
|
|
32
|
+
get_wiki_membership: { tool_group: "wiki_admin", intent_domain: "wiki_management", operation: "wiki_membership_read", entity_type: "wiki", mutation_scope: "none" },
|
|
33
|
+
whoami: { tool_group: "account", intent_domain: "auth", operation: "account_read", entity_type: "account", mutation_scope: "none" },
|
|
34
|
+
};
|
|
35
|
+
function metadataForTool(toolName) {
|
|
36
|
+
return TOOL_METADATA[toolName] ?? {
|
|
37
|
+
tool_group: "account",
|
|
38
|
+
intent_domain: "auth",
|
|
39
|
+
operation: "unknown",
|
|
40
|
+
entity_type: "account",
|
|
41
|
+
mutation_scope: "none",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function errorType(error) {
|
|
45
|
+
if (error instanceof Error && error.name)
|
|
46
|
+
return error.name;
|
|
47
|
+
return typeof error;
|
|
48
|
+
}
|
|
49
|
+
export async function postMcpToolCall(event) {
|
|
50
|
+
await request("POST", "/api/mcp/tool-calls", {
|
|
51
|
+
auth: true,
|
|
52
|
+
json: event,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export function wrapMcpToolDefinition(definition, deps) {
|
|
56
|
+
if (!definition.execute || AUTH_EXEMPT_TOOLS.has(definition.name)) {
|
|
57
|
+
return definition;
|
|
58
|
+
}
|
|
59
|
+
const execute = definition.execute;
|
|
60
|
+
const validateAuth = deps.validateAuth ?? requireValidApiKey;
|
|
61
|
+
const trackToolCall = deps.trackToolCall ?? postMcpToolCall;
|
|
62
|
+
const now = deps.now ?? Date.now;
|
|
63
|
+
const metadata = metadataForTool(definition.name);
|
|
64
|
+
return {
|
|
65
|
+
...definition,
|
|
66
|
+
async execute(...args) {
|
|
67
|
+
await validateAuth();
|
|
68
|
+
const startedAt = now();
|
|
69
|
+
const trackingState = { details: {} };
|
|
70
|
+
let success = false;
|
|
71
|
+
let caught;
|
|
72
|
+
try {
|
|
73
|
+
const result = await trackingDetailsStorage.run(trackingState, async () => execute(...args));
|
|
74
|
+
success = true;
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
caught = error;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
const event = {
|
|
83
|
+
tool_name: definition.name,
|
|
84
|
+
...metadata,
|
|
85
|
+
...trackingState.details,
|
|
86
|
+
success,
|
|
87
|
+
duration_ms: Math.max(0, Math.round(now() - startedAt)),
|
|
88
|
+
mcp_version: deps.mcpVersion,
|
|
89
|
+
};
|
|
90
|
+
if (caught !== undefined) {
|
|
91
|
+
event.error_type = errorType(caught);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
await trackToolCall(event);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Analytics should never change tool behavior.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function installMcpToolTracking(server, mcpVersion) {
|
|
104
|
+
const addTool = server.addTool.bind(server);
|
|
105
|
+
server.addTool =
|
|
106
|
+
(definition) => addTool(wrapMcpToolDefinition(definition, { mcpVersion }));
|
|
107
|
+
return server;
|
|
108
|
+
}
|
package/dist/tools/auth.js
CHANGED
|
@@ -4,7 +4,7 @@ export function registerAuthTools(server) {
|
|
|
4
4
|
server.addTool({
|
|
5
5
|
name: "login",
|
|
6
6
|
description: "Log in via browser to connect your account and get a personal API key. This is the required " +
|
|
7
|
-
"first step before
|
|
7
|
+
"first step before using Almanac MCP tools. Only needs to be called once.\n\n" +
|
|
8
8
|
"If you already have a valid API key, this returns immediately without opening a browser.",
|
|
9
9
|
async execute() {
|
|
10
10
|
const result = await performLogin();
|
|
@@ -3,15 +3,39 @@ import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync } from
|
|
|
3
3
|
import { stringify as yamlStringify } from "yaml";
|
|
4
4
|
import { request } from "../../auth.js";
|
|
5
5
|
import { openBrowser } from "../../browser.js";
|
|
6
|
+
import { setMcpTrackingDetails } from "../../tool-tracking.js";
|
|
6
7
|
import { coerceJson } from "../../utils.js";
|
|
7
8
|
import { formatPublishResults } from "./publish-format.js";
|
|
8
9
|
import { resolvePageDir, resolvePagePaths, SLUG_RE } from "./workspace.js";
|
|
9
10
|
import { WRITING_GUIDE } from "./writing-guide.js";
|
|
11
|
+
function summarizePublishTracking(results, requestedCount, wiki_slug, dryRun) {
|
|
12
|
+
const details = {
|
|
13
|
+
wiki_slug,
|
|
14
|
+
dry_run: dryRun,
|
|
15
|
+
mutation_scope: dryRun ? "backend_dry_run" : "backend",
|
|
16
|
+
requested_count: requestedCount,
|
|
17
|
+
result_count: results.length,
|
|
18
|
+
result_error_count: results.filter(result => result.status === "error").length,
|
|
19
|
+
result_stub_created_count: results.reduce((count, result) => count + (result.stubs_created?.length ?? 0), 0),
|
|
20
|
+
};
|
|
21
|
+
if (dryRun) {
|
|
22
|
+
details.planned_created_count = results.filter(result => result.plan?.action === "create").length;
|
|
23
|
+
details.planned_updated_count = results.filter(result => result.plan?.action === "update").length;
|
|
24
|
+
details.planned_renamed_count = results.filter(result => result.plan?.action === "rename").length;
|
|
25
|
+
details.planned_error_count = results.filter(result => result.plan?.action === "error").length;
|
|
26
|
+
return details;
|
|
27
|
+
}
|
|
28
|
+
details.result_created_count = results.filter(result => result.status === "created").length;
|
|
29
|
+
details.result_updated_count = results.filter(result => result.status === "updated").length;
|
|
30
|
+
details.result_renamed_count = results.filter(result => result.status === "renamed").length;
|
|
31
|
+
details.result_unchanged_count = results.filter(result => result.status === "unchanged").length;
|
|
32
|
+
return details;
|
|
33
|
+
}
|
|
10
34
|
export function registerPageTools(server) {
|
|
11
35
|
server.addTool({
|
|
12
36
|
name: "search_pages",
|
|
13
|
-
description: "Search
|
|
14
|
-
"or discover content. Optional wiki filter to scope results.
|
|
37
|
+
description: "Search Almanac pages and stubs across all wikis. Use to check existence, find slugs for wikilinks, " +
|
|
38
|
+
"or discover content. Optional wiki filter to scope results. Requires login.",
|
|
15
39
|
parameters: z.object({
|
|
16
40
|
queries: coerceJson(z.array(z.string()).min(1).max(20)).describe("Search queries (1-20)"),
|
|
17
41
|
wiki: z.string().optional().describe("Filter to a specific wiki slug"),
|
|
@@ -28,7 +52,7 @@ export function registerPageTools(server) {
|
|
|
28
52
|
};
|
|
29
53
|
if (wiki)
|
|
30
54
|
body.wiki = wiki;
|
|
31
|
-
const resp = await request("POST", "/api/search/batch", { json: body });
|
|
55
|
+
const resp = await request("POST", "/api/search/batch", { auth: true, json: body });
|
|
32
56
|
const data = (await resp.json());
|
|
33
57
|
const byQuery = {};
|
|
34
58
|
for (const set of data.results) {
|
|
@@ -39,8 +63,8 @@ export function registerPageTools(server) {
|
|
|
39
63
|
});
|
|
40
64
|
server.addTool({
|
|
41
65
|
name: "search_topics",
|
|
42
|
-
description: "Search
|
|
43
|
-
"Optional wiki filter to scope results.
|
|
66
|
+
description: "Search Almanac topics across all wikis. Use to discover topic slugs and topic pages. " +
|
|
67
|
+
"Optional wiki filter to scope results. Requires login.",
|
|
44
68
|
parameters: z.object({
|
|
45
69
|
queries: coerceJson(z.array(z.string()).min(1).max(20)).describe("Search queries (1-20)"),
|
|
46
70
|
wiki: z.string().optional().describe("Filter to a specific wiki slug"),
|
|
@@ -54,7 +78,7 @@ export function registerPageTools(server) {
|
|
|
54
78
|
};
|
|
55
79
|
if (wiki)
|
|
56
80
|
params.wiki = wiki;
|
|
57
|
-
const resp = await request("GET", "/api/search", { params });
|
|
81
|
+
const resp = await request("GET", "/api/search", { auth: true, params });
|
|
58
82
|
results[q] = await resp.json();
|
|
59
83
|
}
|
|
60
84
|
return JSON.stringify(results, null, 2);
|
|
@@ -64,7 +88,7 @@ export function registerPageTools(server) {
|
|
|
64
88
|
name: "list_pages",
|
|
65
89
|
description: "Browse pages in a wiki. Structured listing, not fuzzy search. " +
|
|
66
90
|
"Use to see what exists, find stubs, or discover pages by topic. " +
|
|
67
|
-
"Each returned page includes topic objects with both slug and title.",
|
|
91
|
+
"Each returned page includes topic objects with both slug and title. Requires login.",
|
|
68
92
|
parameters: z.object({
|
|
69
93
|
wiki_slug: z.string().describe("Wiki slug"),
|
|
70
94
|
topic: z.string().optional().describe("Filter by topic slug"),
|
|
@@ -78,7 +102,7 @@ export function registerPageTools(server) {
|
|
|
78
102
|
params.topic = topic;
|
|
79
103
|
if (stubs_only)
|
|
80
104
|
params.stub = true;
|
|
81
|
-
const resp = await request("GET", `/api/w/${wiki_slug}/pages`, { params });
|
|
105
|
+
const resp = await request("GET", `/api/w/${wiki_slug}/pages`, { auth: true, params });
|
|
82
106
|
return JSON.stringify(await resp.json(), null, 2);
|
|
83
107
|
},
|
|
84
108
|
});
|
|
@@ -198,6 +222,13 @@ export function registerPageTools(server) {
|
|
|
198
222
|
nudges.length > 0 ? nudges.join("\n") : "",
|
|
199
223
|
WRITING_GUIDE,
|
|
200
224
|
];
|
|
225
|
+
setMcpTrackingDetails({
|
|
226
|
+
wiki_slug,
|
|
227
|
+
requested_count: pages.length,
|
|
228
|
+
result_count: created.length + skipped.length,
|
|
229
|
+
result_created_count: created.length,
|
|
230
|
+
result_skipped_count: skipped.length,
|
|
231
|
+
});
|
|
201
232
|
return parts.filter(Boolean).join("\n\n");
|
|
202
233
|
},
|
|
203
234
|
});
|
|
@@ -253,8 +284,11 @@ export function registerPageTools(server) {
|
|
|
253
284
|
});
|
|
254
285
|
const results = (await resp.json());
|
|
255
286
|
const summary = formatPublishResults(results, targetSlugs, wiki_slug, dry_run ?? false);
|
|
287
|
+
const dryRun = dry_run ?? false;
|
|
288
|
+
const details = summarizePublishTracking(results, targetSlugs.length, wiki_slug, dryRun);
|
|
289
|
+
setMcpTrackingDetails(details);
|
|
256
290
|
// Open browser on single-page publish success (non-GUI, non-dry-run).
|
|
257
|
-
if (!
|
|
291
|
+
if (!dryRun && targetSlugs.length === 1 && process.env.OPENALMANAC_GUI !== "1") {
|
|
258
292
|
const r = results[0];
|
|
259
293
|
if (r && r.status !== "error") {
|
|
260
294
|
const resultSlug = r.slug;
|
|
@@ -272,13 +306,13 @@ export function registerPageTools(server) {
|
|
|
272
306
|
description: "Read a single page by slug. Returns the full page JSON including content, topics, sources, and infobox. " +
|
|
273
307
|
"No side effects — use this to read a page without downloading it to disk or joining the wiki. " +
|
|
274
308
|
"For editing, use `download` instead (it writes local files and handles ref tokens). " +
|
|
275
|
-
"For discovery, use `search_pages` instead.
|
|
309
|
+
"For discovery, use `search_pages` instead. Requires login.",
|
|
276
310
|
parameters: z.object({
|
|
277
311
|
wiki_slug: z.string().describe("Wiki slug"),
|
|
278
312
|
page_slug: z.string().describe("Page slug"),
|
|
279
313
|
}),
|
|
280
314
|
async execute({ wiki_slug, page_slug }) {
|
|
281
|
-
const resp = await request("GET", `/api/w/${wiki_slug}/pages/${page_slug}
|
|
315
|
+
const resp = await request("GET", `/api/w/${wiki_slug}/pages/${page_slug}`, { auth: true });
|
|
282
316
|
return JSON.stringify(await resp.json(), null, 2);
|
|
283
317
|
},
|
|
284
318
|
});
|
package/dist/tools/topics.js
CHANGED
|
@@ -4,13 +4,14 @@ import { coerceJson } from "../utils.js";
|
|
|
4
4
|
export function registerTopicTools(server) {
|
|
5
5
|
server.addTool({
|
|
6
6
|
name: "list_topics",
|
|
7
|
-
description: "List topics in a wiki. Returns flat list or graph (nodes + edges).
|
|
7
|
+
description: "List topics in a wiki. Returns flat list or graph (nodes + edges). Requires login.",
|
|
8
8
|
parameters: z.object({
|
|
9
9
|
wiki_slug: z.string().describe("Wiki slug"),
|
|
10
10
|
format: z.enum(["flat", "graph"]).default("flat").describe("Response format"),
|
|
11
11
|
}),
|
|
12
12
|
async execute({ wiki_slug, format }) {
|
|
13
13
|
const resp = await request("GET", `/api/w/${wiki_slug}/topics`, {
|
|
14
|
+
auth: true,
|
|
14
15
|
params: { format },
|
|
15
16
|
});
|
|
16
17
|
return JSON.stringify(await resp.json(), null, 2);
|
package/dist/tools/wikis.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { request } from "../auth.js";
|
|
3
3
|
import { coerceJson } from "../utils.js";
|
|
4
|
+
const MAX_IMAGE_URL_LENGTH = 2048;
|
|
5
|
+
const httpImageUrlSchema = z.string()
|
|
6
|
+
.max(MAX_IMAGE_URL_LENGTH)
|
|
7
|
+
.refine((value) => !value.startsWith("data:") && !value.includes("base64,"), "Use an http(s) image URL, not inline image data")
|
|
8
|
+
.refine((value) => value.startsWith("http://") || value.startsWith("https://"), "Image URL must start with http:// or https://");
|
|
9
|
+
const coverImageUrlSchema = z.string()
|
|
10
|
+
.max(MAX_IMAGE_URL_LENGTH)
|
|
11
|
+
.refine((value) => value.startsWith("linear-gradient(") ||
|
|
12
|
+
value.startsWith("http://") ||
|
|
13
|
+
value.startsWith("https://"), "Cover image must be an http(s) URL or gallery gradient")
|
|
14
|
+
.refine((value) => value.startsWith("linear-gradient(") ||
|
|
15
|
+
(!value.startsWith("data:") && !value.includes("base64,")), "Use an http(s) image URL, not inline image data");
|
|
4
16
|
// Mirrors backend `NavItem` in src/schemas/wiki_settings_schemas.py. The
|
|
5
17
|
// refinement matches the `exactly_one_target` @model_validator there —
|
|
6
18
|
// agents get a clear error pre-flight instead of a 422 round-trip.
|
|
@@ -34,7 +46,7 @@ const navItemSchema = z.lazy(() => z.object({
|
|
|
34
46
|
const themeSchema = z.object({
|
|
35
47
|
accent_color: z.string().optional(),
|
|
36
48
|
name_font: z.string().optional(),
|
|
37
|
-
logo_url:
|
|
49
|
+
logo_url: httpImageUrlSchema.nullable().optional(),
|
|
38
50
|
cover_tint_intensity: z.number().optional(),
|
|
39
51
|
logo_tint_intensity: z.number().optional(),
|
|
40
52
|
cover_y_offset: z.number().optional(),
|
|
@@ -42,12 +54,13 @@ const themeSchema = z.object({
|
|
|
42
54
|
export function registerWikiTools(server) {
|
|
43
55
|
server.addTool({
|
|
44
56
|
name: "list_wikis",
|
|
45
|
-
description: "List every wiki on
|
|
57
|
+
description: "List every wiki on Almanac. Use before creating a new wiki so you can suggest contributing to an existing one. The global almanac has slug `global` and is excluded by default — pass `include_global: true` to include it. Requires login.",
|
|
46
58
|
parameters: z.object({
|
|
47
59
|
include_global: z.boolean().default(false).describe("Include the global almanac wiki in results"),
|
|
48
60
|
}),
|
|
49
61
|
async execute({ include_global }) {
|
|
50
62
|
const resp = await request("GET", "/api/wikis", {
|
|
63
|
+
auth: true,
|
|
51
64
|
params: { include_global },
|
|
52
65
|
});
|
|
53
66
|
return JSON.stringify(await resp.json(), null, 2);
|
|
@@ -71,23 +84,23 @@ export function registerWikiTools(server) {
|
|
|
71
84
|
});
|
|
72
85
|
server.addTool({
|
|
73
86
|
name: "get_wiki_settings",
|
|
74
|
-
description: "Read a wiki's details and settings (nav, cover, theme).
|
|
87
|
+
description: "Read a wiki's details and settings (nav, cover, theme). Requires login.",
|
|
75
88
|
parameters: z.object({
|
|
76
89
|
wiki_slug: z.string().describe("Wiki slug"),
|
|
77
90
|
}),
|
|
78
91
|
async execute({ wiki_slug }) {
|
|
79
|
-
const resp = await request("GET", `/api/w/${wiki_slug}
|
|
92
|
+
const resp = await request("GET", `/api/w/${wiki_slug}`, { auth: true });
|
|
80
93
|
return JSON.stringify(await resp.json(), null, 2);
|
|
81
94
|
},
|
|
82
95
|
});
|
|
83
96
|
server.addTool({
|
|
84
97
|
name: "update_wiki_settings",
|
|
85
|
-
description: "Update a wiki's settings. Pass any combination of `nav`, `cover_image_url`, and `theme` — omitted fields are preserved (the backend uses exclude_unset merge). For example, `{nav: [...]}` updates navigation only without touching theme or cover_image_url. Each NavItem must have exactly one of `page`, `topic`, or `link`. Use `auto` (only on topic items) to auto-populate children from the topic DAG. Requires moderator access.",
|
|
98
|
+
description: "Update a wiki's settings. Pass any combination of `nav`, `cover_image_url`, and `theme` — omitted fields are preserved (the backend uses exclude_unset merge). For example, `{nav: [...]}` updates navigation only without touching theme or cover_image_url. `cover_image_url` accepts http(s) image URLs or gallery gradients; `theme.logo_url` accepts http(s) image URLs. Do not pass inline base64/data image strings. Each NavItem must have exactly one of `page`, `topic`, or `link`. Use `auto` (only on topic items) to auto-populate children from the topic DAG. Requires moderator access.",
|
|
86
99
|
parameters: z.object({
|
|
87
100
|
wiki_slug: z.string().describe("Wiki slug"),
|
|
88
101
|
settings: coerceJson(z.object({
|
|
89
102
|
nav: z.array(navItemSchema).optional(),
|
|
90
|
-
cover_image_url:
|
|
103
|
+
cover_image_url: coverImageUrlSchema.nullable().optional(),
|
|
91
104
|
theme: themeSchema.optional(),
|
|
92
105
|
})).describe("Settings to update"),
|
|
93
106
|
}),
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|