attachment.mjs
239 lines 8.4 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 10 days ago
1 /**
2 * MCP Attachment read tools — attachment_list / attachment_get (Phase 2F-b-b).
3 *
4 * @see docs/ATTACHMENT-STORE-CONTRACT-2F-b.md §5.2
5 */
6
7 import { z } from 'zod';
8 import { loadConfig } from '../../lib/config.mjs';
9 import {
10 handleAttachmentListRequest,
11 handleAttachmentGetRequest,
12 } from '../../lib/attachments/attachment-handlers.mjs';
13 import {
14 handleMediaLinkProposeRequest,
15 handleMediaAttachProposeRequest,
16 handleMediaImportConsentListRequest,
17 } from '../../lib/attachments/attachment-write.mjs';
18 import { createProposal } from '../../hub/proposals-store.mjs';
19 import { jsonResponse, jsonError } from '../create-server.mjs';
20
21 /**
22 * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
23 */
24 export function registerAttachmentTools(server) {
25 server.registerTool(
26 'attachment_list',
27 {
28 description:
29 'List scope-visible attachments (content-minimized summaries). Same JSON as Hub GET /api/v1/attachments.',
30 inputSchema: {
31 scope: z.enum(['personal', 'project', 'org']).optional().describe('Narrow within authorized scopes only'),
32 note_ref: z.string().optional().describe('Filter attachments linked to note:path'),
33 source: z
34 .enum(['vault_file', 'mist_blob', 'embedded_url', 'connector_ref'])
35 .optional()
36 .describe('Filter by derivation source'),
37 mime_class: z
38 .enum(['image', 'video', 'audio', 'document', 'unknown'])
39 .optional()
40 .describe('Filter by mime class'),
41 storage_kind: z.enum(['vault_blob', 'external_link']).optional().describe('Filter storage kind'),
42 agent_visible: z.boolean().optional().describe('When true, only agent-visible attachments'),
43 limit: z.number().int().min(1).max(500).optional().describe('Max summaries (default 500)'),
44 vault_id: z.string().optional().describe('Vault id (default from config)'),
45 },
46 },
47 async (args) => {
48 try {
49 const config = loadConfig();
50 const vaultId = args.vault_id?.trim() || config.default_vault_id || 'default';
51 const cliScopes = Array.isArray(config.flow?.visible_scopes)
52 ? config.flow.visible_scopes
53 : undefined;
54 const result = handleAttachmentListRequest({
55 dataDir: config.data_dir,
56 vaultPath: config.vault_path,
57 vaultId,
58 cliScopes,
59 scope: args.scope,
60 note_ref: args.note_ref,
61 source: args.source,
62 mime_class: args.mime_class,
63 storage_kind: args.storage_kind,
64 agent_visible: args.agent_visible,
65 limit: args.limit,
66 vaultConfig: { ignore: config.ignore },
67 });
68 if (!result.ok) {
69 return jsonError(result.error, result.code);
70 }
71 return jsonResponse(result.payload);
72 } catch (e) {
73 return jsonError(e.message || String(e), 'RUNTIME_ERROR');
74 }
75 },
76 );
77
78 server.registerTool(
79 'attachment_get',
80 {
81 description:
82 'Get one authorized attachment record. Same JSON as Hub GET /api/v1/attachments/{id}.',
83 inputSchema: {
84 attachment_id: z.string().describe('Attachment id (att_<tag>_<32hex>)'),
85 vault_id: z.string().optional().describe('Vault id (default from config)'),
86 },
87 },
88 async (args) => {
89 try {
90 const config = loadConfig();
91 const vaultId = args.vault_id?.trim() || config.default_vault_id || 'default';
92 const cliScopes = Array.isArray(config.flow?.visible_scopes)
93 ? config.flow.visible_scopes
94 : undefined;
95 const result = handleAttachmentGetRequest({
96 dataDir: config.data_dir,
97 vaultPath: config.vault_path,
98 vaultId,
99 attachmentId: args.attachment_id,
100 cliScopes,
101 vaultConfig: { ignore: config.ignore },
102 });
103 if (!result.ok) {
104 return jsonError(result.error, result.code);
105 }
106 return jsonResponse(result.payload);
107 } catch (e) {
108 return jsonError(e.message || String(e), 'RUNTIME_ERROR');
109 }
110 },
111 );
112
113 server.registerTool(
114 'media_external_link_propose',
115 {
116 description:
117 'Propose linking an external media reference (review-before-write). Same JSON as Hub POST /api/v1/attachments/link-proposals.',
118 inputSchema: {
119 intent: z.string().describe('Human-readable reason (recorded, never executed)'),
120 scope: z.enum(['personal', 'project', 'org']),
121 connector_id: z.string().describe('Allowlisted connector id'),
122 opaque_ref: z.string().describe('Connector-scoped opaque handle (never fetched)'),
123 consent_id: z.string().describe('Active import consent id (mic_…)'),
124 display_label: z.string().optional(),
125 vault_id: z.string().optional(),
126 },
127 },
128 async (args) => {
129 try {
130 const config = loadConfig();
131 const vaultId = args.vault_id?.trim() || config.default_vault_id || 'default';
132 const cliScopes = Array.isArray(config.flow?.visible_scopes)
133 ? config.flow.visible_scopes
134 : undefined;
135 const result = await handleMediaLinkProposeRequest({
136 dataDir: config.data_dir,
137 vaultPath: config.vault_path,
138 vaultId,
139 cliScopes,
140 intent: args.intent,
141 body: {
142 scope: args.scope,
143 connector_id: args.connector_id,
144 opaque_ref: args.opaque_ref,
145 consent_id: args.consent_id,
146 display_label: args.display_label,
147 },
148 createProposal,
149 vaultConfig: { ignore: config.ignore },
150 });
151 if (!result.ok) {
152 return jsonError(result.error, result.code);
153 }
154 return jsonResponse(result.payload);
155 } catch (e) {
156 return jsonError(e.message || String(e), 'RUNTIME_ERROR');
157 }
158 },
159 );
160
161 server.registerTool(
162 'media_attach_propose',
163 {
164 description:
165 'Propose attaching media to a note (review-before-write). Same JSON as Hub POST /api/v1/attachments/attach-proposals.',
166 inputSchema: {
167 intent: z.string(),
168 scope: z.enum(['personal', 'project', 'org']),
169 attachment_id: z.string(),
170 note_ref: z.string().describe('Target note ref (note:path)'),
171 base_state_id: z.string().describe('Target note kn1_ concurrency token'),
172 vault_id: z.string().optional(),
173 },
174 },
175 async (args) => {
176 try {
177 const config = loadConfig();
178 const vaultId = args.vault_id?.trim() || config.default_vault_id || 'default';
179 const cliScopes = Array.isArray(config.flow?.visible_scopes)
180 ? config.flow.visible_scopes
181 : undefined;
182 const result = await handleMediaAttachProposeRequest({
183 dataDir: config.data_dir,
184 vaultPath: config.vault_path,
185 vaultId,
186 cliScopes,
187 intent: args.intent,
188 body: {
189 scope: args.scope,
190 attachment_id: args.attachment_id,
191 note_ref: args.note_ref,
192 base_state_id: args.base_state_id,
193 },
194 createProposal,
195 vaultConfig: { ignore: config.ignore },
196 });
197 if (!result.ok) {
198 return jsonError(result.error, result.code);
199 }
200 return jsonResponse(result.payload);
201 } catch (e) {
202 return jsonError(e.message || String(e), 'RUNTIME_ERROR');
203 }
204 },
205 );
206
207 server.registerTool(
208 'media_import_consent_list',
209 {
210 description:
211 'List import consents for external media linking (read-only). Same JSON as Hub GET /api/v1/attachments/import-consents.',
212 inputSchema: {
213 scope: z.enum(['personal', 'project', 'org']).optional(),
214 vault_id: z.string().optional(),
215 },
216 },
217 async (args) => {
218 try {
219 const config = loadConfig();
220 const vaultId = args.vault_id?.trim() || config.default_vault_id || 'default';
221 const cliScopes = Array.isArray(config.flow?.visible_scopes)
222 ? config.flow.visible_scopes
223 : undefined;
224 const result = handleMediaImportConsentListRequest({
225 dataDir: config.data_dir,
226 vaultId,
227 cliScopes,
228 scope: args.scope,
229 });
230 if (!result.ok) {
231 return jsonError(result.error, result.code);
232 }
233 return jsonResponse(result.payload);
234 } catch (e) {
235 return jsonError(e.message || String(e), 'RUNTIME_ERROR');
236 }
237 },
238 );
239 }
File History 1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago