hub-proposals.mjs
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