feat(mcp-server): add nexus_vision tool and fix upload MIME detection

- New tool nexus_vision: describe an image via a vision chat model
  (default google/gemini-3.1-flash-lite) and record result in journal.
- NordRouter /media/upload now detects MIME from file extension so
  data: URLs use the correct type (fixes HTTP 415 for images/audio/video).
- HttpClient supports FormData bodies (undici) for future multipart use.
This commit is contained in:
OpenCode
2026-08-18 11:50:29 +07:00
parent c65f9106df
commit b2f57cc361
5 changed files with 135 additions and 6 deletions
@@ -136,7 +136,21 @@ export class NordRouterMediaProvider implements MediaProvider {
throw new NexusError('IO', `Upload file not found: ${input.path}`);
}
const buf = fs.readFileSync(input.path);
const mime = input.mimeType || 'application/octet-stream';
const ext = input.path.split('.').pop()?.toLowerCase() || '';
const mimeByExt: Record<string, string> = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
webp: 'image/webp',
gif: 'image/gif',
mp4: 'video/mp4',
mov: 'video/quicktime',
wav: 'audio/wav',
mp3: 'audio/mpeg',
ogg: 'audio/ogg',
m4a: 'audio/mp4',
};
const mime = input.mimeType || mimeByExt[ext] || 'application/octet-stream';
const name = input.name || input.path.split('/').pop() || 'file';
const dataUrl = `data:${mime};base64,${buf.toString('base64')}`;
const res = await this.client.http.requestJson<{ url?: string }>({
+10 -5
View File
@@ -1,4 +1,4 @@
import { fetch, ProxyAgent, type RequestInit, type Response } from 'undici';
import { fetch, ProxyAgent, FormData, type RequestInit, type Response } from 'undici';
import { NexusError, codeFromHttpStatus } from './errors.js';
import { sleep } from './polling.js';
@@ -83,10 +83,15 @@ export class HttpClient {
const headers: Record<string, string> = { ...this.opts.defaultHeaders, ...options.headers };
let bodyStr: string | undefined;
let bodyForm: FormData | undefined;
if (options.body !== undefined) {
bodyStr = typeof options.body === 'string' ? options.body : JSON.stringify(options.body);
if (!headers['Content-Type'] && !headers['content-type']) {
headers['Content-Type'] = 'application/json';
if (options.body instanceof FormData) {
bodyForm = options.body;
} else {
bodyStr = typeof options.body === 'string' ? options.body : JSON.stringify(options.body);
if (!headers['Content-Type'] && !headers['content-type']) {
headers['Content-Type'] = 'application/json';
}
}
}
@@ -96,7 +101,7 @@ export class HttpClient {
const init: RequestInit & { dispatcher?: ProxyAgent } = {
method,
headers,
body: bodyStr,
body: bodyForm ?? bodyStr,
signal: AbortSignal.timeout(timeoutMs),
};
if (this.proxyAgent) init.dispatcher = this.proxyAgent;