Add NexusAI: universal MCP service (TTS/STT/image/video/music/web-search)
Monorepo-lite with shared @nexusai/core and @nexusai/mcp-server (stdio). Provider-agnostic tools with explicit provider selection; NordRouter adapter (media generate/poll/download, estimate, upload, models) and search via perplexity/sonar. Local STT via faster-whisper uv sidecar. SQLite journal of every generation for usage stats and future admin. 10 MCP tools.
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@nexusai/mcp-server",
|
||||
"version": "0.1.0",
|
||||
"description": "NexusAI MCP server (stdio): TTS, STT, image/video/music generation, web search",
|
||||
"main": "dist/index.js",
|
||||
"type": "commonjs",
|
||||
"bin": {
|
||||
"nexusai-mcp": "dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||
"@nexusai/core": "*",
|
||||
"zod": "^3.25.0",
|
||||
"zod-to-json-schema": "^3.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.0",
|
||||
"typescript": "^5.8.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
Journal,
|
||||
MediaStorage,
|
||||
ProviderRegistry,
|
||||
createNordRouterProviders,
|
||||
loadConfig,
|
||||
LocalWhisperProvider,
|
||||
createLogger,
|
||||
type NexusConfig,
|
||||
} from '@nexusai/core';
|
||||
|
||||
const log = createLogger('context');
|
||||
|
||||
export interface AppContext {
|
||||
config: NexusConfig;
|
||||
registry: ProviderRegistry;
|
||||
storage: MediaStorage;
|
||||
journal: Journal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire up all providers, storage and the journal once at startup.
|
||||
*/
|
||||
export function buildContext(): AppContext {
|
||||
const config = loadConfig();
|
||||
const storage = new MediaStorage(config.mediaDir);
|
||||
const journal = new Journal(config.dbPath);
|
||||
const registry = new ProviderRegistry();
|
||||
|
||||
// NordRouter (media + search). Registered even without a key so error messages
|
||||
// are clear when a tool is actually invoked.
|
||||
try {
|
||||
const nr = createNordRouterProviders({ config: config.nordrouter, storage, save: true });
|
||||
registry.registerMedia(nr.media);
|
||||
registry.registerSearch(nr.search);
|
||||
log.info('registered nordrouter provider (media + search)');
|
||||
} catch (err) {
|
||||
log.warn('nordrouter not registered', err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
|
||||
// Local STT (faster-whisper sidecar).
|
||||
registry.registerStt(new LocalWhisperProvider(config.stt));
|
||||
log.info('registered local STT provider');
|
||||
|
||||
return { config, registry, storage, journal };
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import {
|
||||
NexusError,
|
||||
buildEmbeddedContent,
|
||||
createLogger,
|
||||
type Capability,
|
||||
type MediaResult,
|
||||
} from '@nexusai/core';
|
||||
import type { AppContext } from './context.js';
|
||||
import {
|
||||
ImageSchema,
|
||||
MediaEstimateSchema,
|
||||
MediaModelsSchema,
|
||||
MediaUploadSchema,
|
||||
MusicSchema,
|
||||
SttSchema,
|
||||
TtsSchema,
|
||||
UsageStatsSchema,
|
||||
VideoSchema,
|
||||
WebSearchSchema,
|
||||
} from './schemas.js';
|
||||
|
||||
const log = createLogger('handlers');
|
||||
|
||||
type McpContent =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'image'; data: string; mimeType: string }
|
||||
| { type: 'audio'; data: string; mimeType: string };
|
||||
|
||||
export interface McpResult {
|
||||
content: McpContent[];
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
function jsonBlock(obj: unknown): McpContent {
|
||||
return { type: 'text', text: JSON.stringify(obj, null, 2) };
|
||||
}
|
||||
|
||||
function summarize(input: Record<string, unknown>): string {
|
||||
const s = JSON.stringify(input);
|
||||
return s.length > 200 ? s.slice(0, 197) + '...' : s;
|
||||
}
|
||||
|
||||
export class Handlers {
|
||||
constructor(private readonly ctx: AppContext) {}
|
||||
|
||||
private providerName(explicit?: string): string {
|
||||
return explicit || this.ctx.config.defaultProvider;
|
||||
}
|
||||
|
||||
async handle(name: string, args: Record<string, unknown>): Promise<McpResult> {
|
||||
switch (name) {
|
||||
case 'nexus_tts':
|
||||
return this.media('tts', TtsSchema.parse(args));
|
||||
case 'nexus_image':
|
||||
return this.media('image', ImageSchema.parse(args));
|
||||
case 'nexus_video':
|
||||
return this.media('video', VideoSchema.parse(args));
|
||||
case 'nexus_music':
|
||||
return this.media('music', MusicSchema.parse(args));
|
||||
case 'nexus_stt':
|
||||
return this.stt(SttSchema.parse(args));
|
||||
case 'nexus_web_search':
|
||||
return this.search(WebSearchSchema.parse(args));
|
||||
case 'nexus_media_models':
|
||||
return this.models(MediaModelsSchema.parse(args));
|
||||
case 'nexus_media_estimate':
|
||||
return this.estimate(MediaEstimateSchema.parse(args));
|
||||
case 'nexus_media_upload':
|
||||
return this.upload(MediaUploadSchema.parse(args));
|
||||
case 'nexus_usage_stats':
|
||||
return this.usage(UsageStatsSchema.parse(args));
|
||||
default:
|
||||
throw new NexusError('NOT_FOUND', `Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async media(
|
||||
capability: Capability,
|
||||
args: { provider?: string; model: string; input: Record<string, unknown>; output?: { save?: boolean; embed?: boolean } }
|
||||
): Promise<McpResult> {
|
||||
const providerName = this.providerName(args.provider);
|
||||
const provider = this.ctx.registry.getMedia(providerName, capability);
|
||||
const started = Date.now();
|
||||
const id = randomUUID();
|
||||
|
||||
try {
|
||||
const result = await provider.generate(capability, { model: args.model, input: args.input });
|
||||
this.ctx.journal.record({
|
||||
id,
|
||||
provider: providerName,
|
||||
capability,
|
||||
model: args.model,
|
||||
inputSummary: summarize(args.input),
|
||||
status: 'done',
|
||||
costUsd: result.costUsd ?? null,
|
||||
filePath: result.filePath ?? null,
|
||||
resultUrl: result.resultUrl ?? null,
|
||||
error: null,
|
||||
durationMs: Date.now() - started,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
return this.mediaResult(result, args.output?.embed ?? false);
|
||||
} catch (err) {
|
||||
const e = NexusError.from(err);
|
||||
this.ctx.journal.record({
|
||||
id,
|
||||
provider: providerName,
|
||||
capability,
|
||||
model: args.model,
|
||||
inputSummary: summarize(args.input),
|
||||
status: 'failed',
|
||||
costUsd: null,
|
||||
filePath: null,
|
||||
resultUrl: null,
|
||||
error: `${e.code}: ${e.message}`,
|
||||
durationMs: Date.now() - started,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private mediaResult(result: MediaResult, embed: boolean): McpResult {
|
||||
const content: McpContent[] = [
|
||||
jsonBlock({
|
||||
provider: result.provider,
|
||||
capability: result.capability,
|
||||
model: result.model,
|
||||
status: result.status,
|
||||
file_path: result.filePath,
|
||||
mime_type: result.mimeType,
|
||||
result_url: result.resultUrl,
|
||||
result_url_2: result.resultUrl2,
|
||||
cost_usd: result.costUsd,
|
||||
}),
|
||||
];
|
||||
|
||||
if (embed && result.filePath) {
|
||||
const block = buildEmbeddedContent(result.filePath, result.mimeType, this.ctx.config.embedMaxBytes);
|
||||
if (block) content.push(block);
|
||||
else content.push({ type: 'text', text: '(embed skipped: file too large or not embeddable)' });
|
||||
}
|
||||
return { content };
|
||||
}
|
||||
|
||||
private async stt(args: {
|
||||
provider?: string;
|
||||
input: { audioPath?: string; audioUrl?: string; audioBase64?: string; language?: string; model?: string };
|
||||
}): Promise<McpResult> {
|
||||
const providerName = args.provider || 'local';
|
||||
const provider = this.ctx.registry.getStt(providerName);
|
||||
const started = Date.now();
|
||||
const id = randomUUID();
|
||||
try {
|
||||
const res = await provider.transcribe(args.input);
|
||||
this.ctx.journal.record({
|
||||
id,
|
||||
provider: providerName,
|
||||
capability: 'stt',
|
||||
model: args.input.model || 'default',
|
||||
inputSummary: summarize({ ...args.input, audioBase64: args.input.audioBase64 ? '[base64]' : undefined }),
|
||||
status: 'done',
|
||||
costUsd: 0,
|
||||
filePath: args.input.audioPath ?? null,
|
||||
resultUrl: null,
|
||||
error: null,
|
||||
durationMs: Date.now() - started,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
jsonBlock({
|
||||
provider: res.provider,
|
||||
language: res.language,
|
||||
duration_sec: res.durationSec,
|
||||
text: res.text,
|
||||
segments: res.segments,
|
||||
}),
|
||||
],
|
||||
};
|
||||
} catch (err) {
|
||||
const e = NexusError.from(err);
|
||||
this.ctx.journal.record({
|
||||
id,
|
||||
provider: providerName,
|
||||
capability: 'stt',
|
||||
model: args.input.model || 'default',
|
||||
inputSummary: 'stt',
|
||||
status: 'failed',
|
||||
costUsd: null,
|
||||
filePath: null,
|
||||
resultUrl: null,
|
||||
error: `${e.code}: ${e.message}`,
|
||||
durationMs: Date.now() - started,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async search(args: {
|
||||
provider?: string;
|
||||
query: string;
|
||||
model?: string;
|
||||
recency?: 'day' | 'week' | 'month' | 'year';
|
||||
maxResults?: number;
|
||||
}): Promise<McpResult> {
|
||||
const providerName = this.providerName(args.provider);
|
||||
const provider = this.ctx.registry.getSearch(providerName);
|
||||
const started = Date.now();
|
||||
const id = randomUUID();
|
||||
const res = await provider.search({
|
||||
query: args.query,
|
||||
model: args.model,
|
||||
recency: args.recency,
|
||||
maxResults: args.maxResults,
|
||||
});
|
||||
this.ctx.journal.record({
|
||||
id,
|
||||
provider: providerName,
|
||||
capability: 'search',
|
||||
model: res.model,
|
||||
inputSummary: summarize({ query: args.query }),
|
||||
status: 'done',
|
||||
costUsd: res.costUsd ?? null,
|
||||
filePath: null,
|
||||
resultUrl: null,
|
||||
error: null,
|
||||
durationMs: Date.now() - started,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
return {
|
||||
content: [jsonBlock({ provider: res.provider, model: res.model, answer: res.answer, citations: res.citations })],
|
||||
};
|
||||
}
|
||||
|
||||
private async models(args: { provider?: string; type?: string; search?: string }): Promise<McpResult> {
|
||||
const providerName = this.providerName(args.provider);
|
||||
// any capability that this provider supports is fine to fetch the catalog
|
||||
const provider = this.ctx.registry.getMedia(providerName, 'image');
|
||||
if (!provider.listModels) throw new NexusError('UNSUPPORTED', `Provider ${providerName} cannot list models`);
|
||||
let models = await provider.listModels();
|
||||
if (args.type) models = models.filter((m) => m.type === args.type);
|
||||
if (args.search) {
|
||||
const q = args.search.toLowerCase();
|
||||
models = models.filter((m) => m.id.toLowerCase().includes(q) || (m.label || '').toLowerCase().includes(q));
|
||||
}
|
||||
return { content: [jsonBlock({ count: models.length, models })] };
|
||||
}
|
||||
|
||||
private async estimate(args: { provider?: string; model: string; input: Record<string, unknown> }): Promise<McpResult> {
|
||||
const providerName = this.providerName(args.provider);
|
||||
const provider = this.ctx.registry.getMedia(providerName, 'image');
|
||||
if (!provider.estimate) throw new NexusError('UNSUPPORTED', `Provider ${providerName} cannot estimate`);
|
||||
const est = await provider.estimate({ model: args.model, input: args.input });
|
||||
return { content: [jsonBlock({ model: args.model, usd: est.usd })] };
|
||||
}
|
||||
|
||||
private async upload(args: { provider?: string; path: string; name?: string; mimeType?: string }): Promise<McpResult> {
|
||||
const providerName = this.providerName(args.provider);
|
||||
const provider = this.ctx.registry.getMedia(providerName, 'image');
|
||||
if (!provider.upload) throw new NexusError('UNSUPPORTED', `Provider ${providerName} cannot upload`);
|
||||
const res = await provider.upload({ path: args.path, name: args.name, mimeType: args.mimeType });
|
||||
return { content: [jsonBlock({ url: res.url, ref: res.ref })] };
|
||||
}
|
||||
|
||||
private async usage(args: { sinceDays?: number; list?: boolean; limit: number }): Promise<McpResult> {
|
||||
const stats = this.ctx.journal.stats(args.sinceDays);
|
||||
const payload: Record<string, unknown> = { stats };
|
||||
if (args.list) payload.recent = this.ctx.journal.list(args.limit);
|
||||
log.debug('usage stats requested');
|
||||
return { content: [jsonBlock(payload)] };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
type Tool,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { NexusError, createLogger } from '@nexusai/core';
|
||||
import { buildContext } from './context.js';
|
||||
import { Handlers } from './handlers.js';
|
||||
import { TOOLS } from './tools.js';
|
||||
|
||||
const log = createLogger('server');
|
||||
|
||||
function main(): void {
|
||||
const ctx = buildContext();
|
||||
const handlers = new Handlers(ctx);
|
||||
|
||||
const server = new Server(
|
||||
{ name: 'nexusai-mcp-server', version: '0.1.0' },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: TOOLS.map((t) => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
inputSchema: t.inputSchema,
|
||||
})) as Tool[],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
try {
|
||||
return (await handlers.handle(name, args ?? {})) as never;
|
||||
} catch (error) {
|
||||
const e = NexusError.from(error);
|
||||
log.error(`tool ${name} failed`, `${e.code}: ${e.message}`);
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error [${e.code}]: ${e.message}` }],
|
||||
isError: true,
|
||||
} as never;
|
||||
}
|
||||
});
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
server.connect(transport).catch((error: unknown) => {
|
||||
log.error('failed to start MCP server', error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
log.info(`NexusAI MCP server started with ${TOOLS.length} tools`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,138 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/** Common output options for media tools. */
|
||||
const OutputOptions = z
|
||||
.object({
|
||||
save: z.boolean().default(true).describe('Save the result to a local file.'),
|
||||
embed: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe('Also embed image/audio directly in the MCP result (size-limited).'),
|
||||
})
|
||||
.partial()
|
||||
.default({});
|
||||
|
||||
const provider = z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Provider name. Defaults to the configured default provider (e.g. "nordrouter").');
|
||||
|
||||
export const TtsSchema = z.object({
|
||||
provider,
|
||||
model: z
|
||||
.string()
|
||||
.default('audio/elevenlabs-tts')
|
||||
.describe('TTS model id, e.g. audio/elevenlabs-tts, audio/elevenlabs-multilingual, audio/gemini-3.1-flash-tts.'),
|
||||
input: z
|
||||
.object({
|
||||
text: z.string().describe('Text to synthesize.'),
|
||||
voice: z.string().optional().describe('Voice name (see nexus_media_models for options).'),
|
||||
voice_id: z.string().optional(),
|
||||
speed: z.union([z.string(), z.number()]).optional().describe('Speed, e.g. "0.8" | "1.0" | "1.2".'),
|
||||
})
|
||||
.passthrough()
|
||||
.describe('Provider input. Extra model-specific fields are passed through.'),
|
||||
output: OutputOptions,
|
||||
});
|
||||
|
||||
export const ImageSchema = z.object({
|
||||
provider,
|
||||
model: z
|
||||
.string()
|
||||
.default('image/nano-banana-2')
|
||||
.describe('Image model id, e.g. image/nano-banana-2, image/flux2-pro, image/gpt-image-2, image/z-image. Use *-edit models for editing.'),
|
||||
input: z
|
||||
.object({
|
||||
prompt: z.string().describe('Image description. For edits also pass "image".'),
|
||||
image: z.string().optional().describe('Source image URL for edit/i2i models.'),
|
||||
aspect_ratio: z.string().optional(),
|
||||
resolution: z.string().optional().describe('1K | 2K | 4K (model dependent).'),
|
||||
})
|
||||
.passthrough(),
|
||||
output: OutputOptions,
|
||||
});
|
||||
|
||||
export const VideoSchema = z.object({
|
||||
provider,
|
||||
model: z
|
||||
.string()
|
||||
.default('video/kling-2.6-t2v')
|
||||
.describe('Video model id, e.g. video/kling-2.6-t2v, video/veo-3.1-fast, video/seedance-2. Use i2v models with an "image".'),
|
||||
input: z
|
||||
.object({
|
||||
prompt: z.string().describe('Video description.'),
|
||||
image: z.string().optional().describe('First-frame image URL for i2v models.'),
|
||||
image2: z.string().optional().describe('Last-frame image URL (transition models).'),
|
||||
duration: z.union([z.string(), z.number()]).optional(),
|
||||
resolution: z.string().optional(),
|
||||
aspect_ratio: z.string().optional(),
|
||||
sound: z.boolean().optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
output: OutputOptions,
|
||||
});
|
||||
|
||||
export const MusicSchema = z.object({
|
||||
provider,
|
||||
model: z
|
||||
.string()
|
||||
.default('music/suno-v5')
|
||||
.describe('Music model id, e.g. music/suno-v5, music/suno-cover, music/suno-sounds.'),
|
||||
input: z
|
||||
.object({
|
||||
title: z.string().optional(),
|
||||
style: z.string().optional(),
|
||||
prompt: z.string().optional().describe('Lyrics/prompt.'),
|
||||
instrumental: z.boolean().optional(),
|
||||
version: z.string().optional().describe('V4 | V4_5 | V4_5PLUS | V4_5ALL | V5 | V5_5.'),
|
||||
})
|
||||
.passthrough(),
|
||||
output: OutputOptions,
|
||||
});
|
||||
|
||||
export const SttSchema = z.object({
|
||||
provider: z.string().optional().describe('STT provider. Defaults to "local" (faster-whisper).'),
|
||||
input: z.object({
|
||||
audioPath: z.string().optional().describe('Local audio file path.'),
|
||||
audioUrl: z.string().optional().describe('Remote audio URL.'),
|
||||
audioBase64: z.string().optional().describe('Base64 or data-URL audio.'),
|
||||
language: z.string().optional().describe('Language hint, e.g. "ru", "en". Auto-detect if omitted.'),
|
||||
model: z.string().optional().describe('Whisper size: tiny|base|small|medium|large-v3.'),
|
||||
}),
|
||||
});
|
||||
|
||||
export const WebSearchSchema = z.object({
|
||||
provider,
|
||||
query: z.string().describe('Search query / question.'),
|
||||
model: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Search model, e.g. perplexity/sonar, perplexity/sonar-pro, or search/<model>.'),
|
||||
recency: z.enum(['day', 'week', 'month', 'year']).optional(),
|
||||
maxResults: z.number().int().positive().max(50).optional(),
|
||||
});
|
||||
|
||||
export const MediaModelsSchema = z.object({
|
||||
provider,
|
||||
type: z.enum(['image', 'video', 'music', 'audio', 'upscale']).optional().describe('Filter by model type.'),
|
||||
search: z.string().optional().describe('Substring filter on model id/label.'),
|
||||
});
|
||||
|
||||
export const MediaEstimateSchema = z.object({
|
||||
provider,
|
||||
model: z.string().describe('Model id to estimate.'),
|
||||
input: z.record(z.unknown()).default({}).describe('Same input you would pass to generate.'),
|
||||
});
|
||||
|
||||
export const MediaUploadSchema = z.object({
|
||||
provider,
|
||||
path: z.string().describe('Local file path to upload.'),
|
||||
name: z.string().optional(),
|
||||
mimeType: z.string().optional(),
|
||||
});
|
||||
|
||||
export const UsageStatsSchema = z.object({
|
||||
sinceDays: z.number().int().positive().optional().describe('Limit stats to the last N days.'),
|
||||
list: z.boolean().default(false).describe('Also return recent generations.'),
|
||||
limit: z.number().int().positive().max(200).default(20),
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import {
|
||||
ImageSchema,
|
||||
MediaEstimateSchema,
|
||||
MediaModelsSchema,
|
||||
MediaUploadSchema,
|
||||
MusicSchema,
|
||||
SttSchema,
|
||||
TtsSchema,
|
||||
UsageStatsSchema,
|
||||
VideoSchema,
|
||||
WebSearchSchema,
|
||||
} from './schemas.js';
|
||||
|
||||
export interface ToolDef {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: object;
|
||||
}
|
||||
|
||||
function make(name: string, description: string, schema: unknown): ToolDef {
|
||||
const json = zodToJsonSchema(schema as any, { name, $refStrategy: 'none' }) as any;
|
||||
const def = json.definitions?.[name] ?? json;
|
||||
return { name, description, inputSchema: def };
|
||||
}
|
||||
|
||||
export const TOOLS: ToolDef[] = [
|
||||
make(
|
||||
'nexus_tts',
|
||||
'Text-to-speech. Generates spoken audio from text via a provider. Returns a saved audio file path and optional embedded audio.',
|
||||
TtsSchema
|
||||
),
|
||||
make(
|
||||
'nexus_image',
|
||||
'Generate or edit images from a text prompt (and optional source image URL). Returns a saved image file and optional embedded image.',
|
||||
ImageSchema
|
||||
),
|
||||
make(
|
||||
'nexus_video',
|
||||
'Generate video from text or an image (t2v / i2v). Async, may take minutes. Returns a saved MP4 file path.',
|
||||
VideoSchema
|
||||
),
|
||||
make(
|
||||
'nexus_music',
|
||||
'Generate music from a style/prompt. Returns saved audio file(s); some models produce two tracks.',
|
||||
MusicSchema
|
||||
),
|
||||
make(
|
||||
'nexus_stt',
|
||||
'Speech-to-text. Transcribe local/remote/base64 audio using a local whisper model. Returns text, language and segments.',
|
||||
SttSchema
|
||||
),
|
||||
make(
|
||||
'nexus_web_search',
|
||||
'Web search that returns an answer with cited source URLs (via search-augmented models).',
|
||||
WebSearchSchema
|
||||
),
|
||||
make(
|
||||
'nexus_media_models',
|
||||
'List available media models (id, type, label, estimated price) for a provider. Use to discover valid model ids and parameters.',
|
||||
MediaModelsSchema
|
||||
),
|
||||
make(
|
||||
'nexus_media_estimate',
|
||||
'Estimate the USD cost of a media generation before running it.',
|
||||
MediaEstimateSchema
|
||||
),
|
||||
make(
|
||||
'nexus_media_upload',
|
||||
'Upload a local media file to the provider and get a reference URL for use as input (image/audio/video).',
|
||||
MediaUploadSchema
|
||||
),
|
||||
make(
|
||||
'nexus_usage_stats',
|
||||
'Show usage and spend recorded in the local journal (totals by provider, capability, day) and optionally recent generations.',
|
||||
UsageStatsSchema
|
||||
),
|
||||
];
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user