hub-proposals.mjs
190 lines 6.7 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Hub proposal tools — require KNOWTATION_HUB_URL and KNOWTATION_HUB_TOKEN (JWT).
3 */
4
5 import { z } from 'zod';
6 import { jsonResponse, jsonError } from '../create-server.mjs';
7
8 function hubBase() {
9 const u = (process.env.KNOWTATION_HUB_URL || '').trim().replace(/\/$/, '');
10 return u || '';
11 }
12
13 function hubHeaders(vaultId) {
14 const token = (process.env.KNOWTATION_HUB_TOKEN || '').trim();
15 if (!token) throw new Error('KNOWTATION_HUB_TOKEN is not set');
16 const h = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token };
17 const vid = (vaultId || process.env.KNOWTATION_HUB_VAULT_ID || '').trim();
18 if (vid) h['X-Vault-Id'] = vid;
19 return h;
20 }
21
22 async function hubFetch(path, { method = 'GET', body, vaultId } = {}) {
23 const base = hubBase();
24 if (!base) throw new Error('KNOWTATION_HUB_URL is not set');
25 const res = await fetch(base + path, {
26 method,
27 headers: hubHeaders(vaultId),
28 body: body != null ? JSON.stringify(body) : undefined,
29 });
30 const text = await res.text();
31 let data;
32 try {
33 data = text ? JSON.parse(text) : null;
34 } catch {
35 data = { error: text.slice(0, 200) };
36 }
37 if (!res.ok) {
38 const msg = data?.error || res.statusText || 'Hub request failed';
39 const err = new Error(msg);
40 err.status = res.status;
41 err.code = data?.code;
42 throw err;
43 }
44 return data;
45 }
46
47 /**
48 * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
49 */
50 export function registerHubProposalTools(server) {
51 server.registerTool(
52 'hub_list_proposals',
53 {
54 description:
55 'List proposals on the Knowtation Hub (requires KNOWTATION_HUB_URL + KNOWTATION_HUB_TOKEN). Respects Hub roles.',
56 inputSchema: {
57 status: z.enum(['proposed', 'approved', 'discarded']).optional(),
58 limit: z.number().optional(),
59 offset: z.number().optional(),
60 label: z.string().optional(),
61 source: z.string().optional(),
62 path_prefix: z.string().optional(),
63 evaluation_status: z
64 .enum(['none', 'pending', 'passed', 'failed', 'needs_changes'])
65 .optional()
66 .describe('Filter by proposal evaluation_status'),
67 review_queue: z.string().optional().describe('Filter by review_queue metadata'),
68 review_severity: z.enum(['standard', 'elevated']).optional().describe('Filter by review_severity'),
69 vault_id: z.string().optional().describe('Sets X-Vault-Id when provided'),
70 },
71 },
72 async (args) => {
73 try {
74 const q = new URLSearchParams();
75 if (args.status) q.set('status', args.status);
76 q.set('limit', String(args.limit ?? 20));
77 if (args.offset != null) q.set('offset', String(args.offset));
78 if (args.label) q.set('label', args.label);
79 if (args.source) q.set('source', args.source);
80 if (args.path_prefix) q.set('path_prefix', args.path_prefix);
81 if (args.evaluation_status) q.set('evaluation_status', args.evaluation_status);
82 if (args.review_queue) q.set('review_queue', args.review_queue);
83 if (args.review_severity) q.set('review_severity', args.review_severity);
84 const path = '/api/v1/proposals?' + q.toString();
85 const out = await hubFetch(path, { vaultId: args.vault_id });
86 return jsonResponse(out);
87 } catch (e) {
88 return jsonError(e.message || String(e), e.code || 'HUB_ERROR');
89 }
90 },
91 );
92
93 server.registerTool(
94 'hub_get_proposal',
95 {
96 description: 'Get one proposal by id from the Hub (metadata + body + frontmatter).',
97 inputSchema: {
98 proposal_id: z.string().describe('Proposal UUID from hub_list_proposals'),
99 vault_id: z.string().optional(),
100 },
101 },
102 async (args) => {
103 try {
104 const out = await hubFetch('/api/v1/proposals/' + encodeURIComponent(args.proposal_id), {
105 vaultId: args.vault_id,
106 });
107 return jsonResponse(out);
108 } catch (e) {
109 return jsonError(e.message || String(e), e.code || 'HUB_ERROR');
110 }
111 },
112 );
113
114 server.registerTool(
115 'hub_create_proposal',
116 {
117 description:
118 'Create a proposal on the Hub (editor/admin JWT). Sends path, body, frontmatter, optional intent, base_state_id, labels, source, external_ref.',
119 inputSchema: {
120 path: z.string().describe('Vault-relative note path'),
121 body: z.string().optional(),
122 frontmatter: z.record(z.unknown()).optional(),
123 intent: z.string().optional(),
124 base_state_id: z.string().optional().describe('kn1_… id from hub note state for optimistic concurrency'),
125 external_ref: z.string().optional(),
126 labels: z.array(z.string()).optional(),
127 source: z.string().optional(),
128 vault_id: z.string().optional(),
129 },
130 },
131 async (args) => {
132 try {
133 const payload = {
134 path: args.path,
135 body: args.body ?? '',
136 frontmatter: args.frontmatter ?? {},
137 intent: args.intent,
138 base_state_id: args.base_state_id,
139 external_ref: args.external_ref,
140 labels: args.labels,
141 source: args.source,
142 };
143 const out = await hubFetch('/api/v1/proposals', {
144 method: 'POST',
145 body: payload,
146 vaultId: args.vault_id,
147 });
148 return jsonResponse(out);
149 } catch (e) {
150 return jsonError(e.message || String(e), e.code || 'HUB_ERROR');
151 }
152 },
153 );
154
155 server.registerTool(
156 'hub_submit_proposal_evaluation',
157 {
158 description:
159 'Submit a human evaluation for a Hub proposal (admin JWT). Requires KNOWTATION_HUB_URL + KNOWTATION_HUB_TOKEN. Outcome pass|fail|needs_changes; comment required for fail and needs_changes; checklist items must all pass for outcome pass when checklist is non-empty.',
160 inputSchema: {
161 proposal_id: z.string().describe('Proposal id from hub_list_proposals'),
162 outcome: z.enum(['pass', 'fail', 'needs_changes']),
163 checklist: z
164 .array(z.object({ id: z.string(), passed: z.boolean() }))
165 .optional()
166 .describe('Rubric row toggles; ids should match GET /settings proposal_rubric.items'),
167 grade: z.string().optional(),
168 comment: z.string().optional(),
169 vault_id: z.string().optional(),
170 },
171 },
172 async (args) => {
173 try {
174 const payload = {
175 outcome: args.outcome,
176 checklist: args.checklist,
177 grade: args.grade,
178 comment: args.comment,
179 };
180 const out = await hubFetch(
181 '/api/v1/proposals/' + encodeURIComponent(args.proposal_id) + '/evaluation',
182 { method: 'POST', body: payload, vaultId: args.vault_id },
183 );
184 return jsonResponse(out);
185 } catch (e) {
186 return jsonError(e.message || String(e), e.code || 'HUB_ERROR');
187 }
188 },
189 );
190 }
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago