mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 01:43:43 +00:00
feat: add MCP server for AI agent integration
Adds a Streamable HTTP MCP server at /mcp that exposes 19 tools covering rundown entries, rundown management, runtime state, project info, automations, and custom fields. Includes Bearer token auth support and a UI card in Settings > Sharing showing the endpoint URL and Claude Desktop config. https://claude.ai/code/session_01U24MeuUacYXeQhbX3tatEe
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||
import { isOntimeCloud } from '../../../../externals';
|
||||
import { generateUrl } from '../../../../common/api/session';
|
||||
import CopyTag from '../../../../common/components/copy-tag/CopyTag';
|
||||
import useInfo from '../../../../common/hooks-query/useInfo';
|
||||
import { isOntimeCloud, serverURL } from '../../../../externals';
|
||||
import GenerateLinkFormExport from '../../../sharing/GenerateLinkFormExport';
|
||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
@@ -11,6 +16,30 @@ export default function FeaturePanel({ location }: PanelBaseProps) {
|
||||
const presetsRef = useScrollIntoView<HTMLDivElement>('presets', location);
|
||||
const linkRef = useScrollIntoView<HTMLDivElement>('link', location);
|
||||
const reportRef = useScrollIntoView<HTMLDivElement>('report', location);
|
||||
const mcpRef = useScrollIntoView<HTMLDivElement>('mcp', location);
|
||||
|
||||
const { data: infoData } = useInfo();
|
||||
const [mcpEndpointUrl, setMcpEndpointUrl] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const baseUrl = isOntimeCloud
|
||||
? serverURL
|
||||
: infoData.networkInterfaces.length > 0
|
||||
? `http://${infoData.networkInterfaces[0].address}:${infoData.serverPort}`
|
||||
: serverURL;
|
||||
|
||||
generateUrl({ baseUrl, path: '/mcp', authenticate: true, lockConfig: false, lockNav: false })
|
||||
.then(setMcpEndpointUrl)
|
||||
.catch(() => {
|
||||
setMcpEndpointUrl(`${baseUrl}/mcp`);
|
||||
});
|
||||
}, [infoData]);
|
||||
|
||||
const claudeDesktopConfig = JSON.stringify(
|
||||
{ mcpServers: { ontime: { url: mcpEndpointUrl } } },
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -33,6 +62,32 @@ export default function FeaturePanel({ location }: PanelBaseProps) {
|
||||
</Panel.Card>
|
||||
</Panel.Section>
|
||||
</div>
|
||||
<div ref={mcpRef}>
|
||||
<Panel.Section>
|
||||
<Panel.Card>
|
||||
<Panel.SubHeader>MCP Server (AI Agent Integration)</Panel.SubHeader>
|
||||
<Panel.Paragraph>
|
||||
Connect AI agents (e.g. Claude Desktop) to Ontime via the Model Context Protocol endpoint.
|
||||
</Panel.Paragraph>
|
||||
<Panel.Divider />
|
||||
<Panel.Field
|
||||
title='MCP Endpoint URL'
|
||||
description='Use this URL in your MCP client configuration'
|
||||
/>
|
||||
{mcpEndpointUrl && (
|
||||
<CopyTag copyValue={mcpEndpointUrl}>{mcpEndpointUrl}</CopyTag>
|
||||
)}
|
||||
<Panel.Divider />
|
||||
<Panel.Field
|
||||
title='Claude Desktop config'
|
||||
description='Paste this into your claude_desktop_config.json under "mcpServers"'
|
||||
/>
|
||||
{mcpEndpointUrl && (
|
||||
<CopyTag copyValue={claudeDesktopConfig}>{claudeDesktopConfig}</CopyTag>
|
||||
)}
|
||||
</Panel.Card>
|
||||
</Panel.Section>
|
||||
</div>
|
||||
<div ref={reportRef}>
|
||||
<ReportSettings />
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"exports": "./src/index.js",
|
||||
"dependencies": {
|
||||
"@googleapis/sheets": "^5.0.5",
|
||||
"@modelcontextprotocol/sdk": "^1.15.0",
|
||||
"cookie": "1.0.2",
|
||||
"cookie-parser": "1.4.7",
|
||||
"cors": "2.8.6",
|
||||
|
||||
@@ -0,0 +1,591 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import express from 'express';
|
||||
import { SupportedEntry } from 'ontime-types';
|
||||
|
||||
import { getAutomationSettings, addAutomation } from '../api-data/automation/automation.dao.js';
|
||||
import { editCurrentProjectData, getProjectData } from '../api-data/project-data/projectData.dao.js';
|
||||
import {
|
||||
getCurrentRundown,
|
||||
getRundownMetadata,
|
||||
getProjectCustomFields,
|
||||
} from '../api-data/rundown/rundown.dao.js';
|
||||
import {
|
||||
addEntry,
|
||||
editEntry,
|
||||
deleteEntries,
|
||||
reorderEntry,
|
||||
loadRundown,
|
||||
initRundown,
|
||||
} from '../api-data/rundown/rundown.service.js';
|
||||
import { duplicateRundown, normalisedToRundownArray } from '../api-data/rundown/rundown.utils.js';
|
||||
import { getDataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { makeNewRundown } from '../models/dataModel.js';
|
||||
import { getState } from '../stores/runtimeState.js';
|
||||
|
||||
// MCP SDK imports
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import {
|
||||
isInitializeRequest,
|
||||
ListToolsRequestSchema,
|
||||
CallToolRequestSchema,
|
||||
ListPromptsRequestSchema,
|
||||
GetPromptRequestSchema,
|
||||
CallToolResult,
|
||||
ListToolsResult,
|
||||
ListPromptsResult,
|
||||
GetPromptResult,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
/** Active sessions indexed by session ID */
|
||||
const sessions = new Map<string, StreamableHTTPServerTransport>();
|
||||
|
||||
// ---- Tool definitions ----
|
||||
const TOOL_DEFINITIONS = [
|
||||
{
|
||||
name: 'get_rundown',
|
||||
description: 'Get the currently loaded rundown including order and entries',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'get_rundown_metadata',
|
||||
description: 'Get cached metadata for the current rundown',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'get_event',
|
||||
description: 'Get a single event by id or cue',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Event ID' },
|
||||
cue: { type: 'string', description: 'Event cue' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'create_event',
|
||||
description: 'Create a new event in the rundown',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['cue', 'title', 'timeStart', 'timeEnd', 'duration'],
|
||||
properties: {
|
||||
cue: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
timeStart: { type: 'number', description: 'Start time in ms from midnight' },
|
||||
timeEnd: { type: 'number', description: 'End time in ms from midnight' },
|
||||
duration: { type: 'number', description: 'Duration in ms' },
|
||||
after: { type: 'string', description: 'Insert after this event ID' },
|
||||
before: { type: 'string', description: 'Insert before this event ID' },
|
||||
note: { type: 'string' },
|
||||
colour: { type: 'string' },
|
||||
skip: { type: 'boolean' },
|
||||
timerType: { type: 'string', enum: ['count-down', 'count-up', 'time-to-end', 'clock'] },
|
||||
endAction: { type: 'string', enum: ['none', 'stop', 'load-next', 'play-next'] },
|
||||
linkStart: { type: 'boolean' },
|
||||
countToEnd: { type: 'boolean' },
|
||||
timeWarning: { type: 'number' },
|
||||
timeDanger: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'update_event',
|
||||
description: 'Update fields of an existing event',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['id'],
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
cue: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
timeStart: { type: 'number' },
|
||||
timeEnd: { type: 'number' },
|
||||
duration: { type: 'number' },
|
||||
note: { type: 'string' },
|
||||
colour: { type: 'string' },
|
||||
skip: { type: 'boolean' },
|
||||
timerType: { type: 'string', enum: ['count-down', 'count-up', 'time-to-end', 'clock'] },
|
||||
endAction: { type: 'string', enum: ['none', 'stop', 'load-next', 'play-next'] },
|
||||
linkStart: { type: 'boolean' },
|
||||
countToEnd: { type: 'boolean' },
|
||||
timeWarning: { type: 'number' },
|
||||
timeDanger: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete_event',
|
||||
description: 'Delete one or more events from the rundown',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['ids'],
|
||||
properties: {
|
||||
ids: { type: 'array', items: { type: 'string' }, description: 'Array of event IDs to delete' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'reorder_event',
|
||||
description: 'Move an event to a new position relative to another event',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['entryId', 'destinationId', 'order'],
|
||||
properties: {
|
||||
entryId: { type: 'string', description: 'ID of the event to move' },
|
||||
destinationId: { type: 'string', description: 'ID of the destination event' },
|
||||
order: { type: 'string', enum: ['before', 'after', 'insert'], description: 'Position relative to destination' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_rundowns',
|
||||
description: 'List all available rundowns',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'create_rundown',
|
||||
description: 'Create a new rundown',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['title'],
|
||||
properties: {
|
||||
title: { type: 'string', description: 'Title for the new rundown' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'load_rundown',
|
||||
description: 'Load a rundown, making it the active rundown',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['id'],
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Rundown ID to load' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'rename_rundown',
|
||||
description: 'Rename an existing rundown',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['id', 'title'],
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Rundown ID to rename' },
|
||||
title: { type: 'string', description: 'New title' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete_rundown',
|
||||
description: 'Delete a rundown (cannot delete the currently loaded rundown or the last remaining rundown)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['id'],
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Rundown ID to delete' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'duplicate_rundown',
|
||||
description: 'Duplicate a rundown',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['id'],
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Rundown ID to duplicate' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_timer_state',
|
||||
description: 'Get the current timer/playback state',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'get_project_info',
|
||||
description: 'Get current project information',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'update_project_info',
|
||||
description: 'Update project information fields',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
publicUrl: { type: 'string' },
|
||||
publicInfo: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_automations',
|
||||
description: 'List all automations and triggers',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'create_automation',
|
||||
description: 'Create a new automation',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['title', 'filterRule', 'filters', 'outputs'],
|
||||
properties: {
|
||||
title: { type: 'string' },
|
||||
filterRule: { type: 'string', enum: ['all', 'any'] },
|
||||
filters: { type: 'array', items: { type: 'object' }, description: 'Array of filter objects' },
|
||||
outputs: { type: 'array', items: { type: 'object' }, description: 'Array of output action objects' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_custom_fields',
|
||||
description: 'Get the project custom fields',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
] as const;
|
||||
|
||||
// ---- Helper to build rundown list response ----
|
||||
function rundownListResponse() {
|
||||
const loaded = getCurrentRundown().id;
|
||||
const rundowns = normalisedToRundownArray(getDataProvider().getProjectRundowns());
|
||||
return { loaded, rundowns };
|
||||
}
|
||||
|
||||
// ---- Tool handlers ----
|
||||
async function handleToolCall(name: string, args: Record<string, unknown>): Promise<CallToolResult> {
|
||||
const text = (data: unknown) => JSON.stringify(data);
|
||||
const ok = (data: unknown): CallToolResult => ({ content: [{ type: 'text', text: text(data) }] });
|
||||
const err = (e: unknown): CallToolResult => ({ content: [{ type: 'text', text: text({ error: String(e) }) }], isError: true });
|
||||
|
||||
switch (name) {
|
||||
case 'get_rundown': {
|
||||
const rundown = getCurrentRundown();
|
||||
return ok({ order: rundown.order, entries: rundown.entries });
|
||||
}
|
||||
|
||||
case 'get_rundown_metadata': {
|
||||
return ok(getRundownMetadata());
|
||||
}
|
||||
|
||||
case 'get_event': {
|
||||
const rundown = getCurrentRundown();
|
||||
const id = args.id as string | undefined;
|
||||
const cue = args.cue as string | undefined;
|
||||
if (id) {
|
||||
const entry = rundown.entries[id];
|
||||
if (!entry) return err(`No event with id ${id}`);
|
||||
return ok(entry);
|
||||
}
|
||||
if (cue) {
|
||||
const entry = Object.values(rundown.entries).find((e) => 'cue' in e && (e as { cue: string }).cue === cue);
|
||||
if (!entry) return err(`No event with cue ${cue}`);
|
||||
return ok(entry);
|
||||
}
|
||||
return err('Provide id or cue');
|
||||
}
|
||||
|
||||
case 'create_event': {
|
||||
try {
|
||||
const entry = await addEntry({ type: SupportedEntry.Event, ...args } as never);
|
||||
return ok(entry);
|
||||
} catch (e) {
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
|
||||
case 'update_event': {
|
||||
try {
|
||||
const entry = await editEntry(args as never);
|
||||
return ok(entry);
|
||||
} catch (e) {
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
|
||||
case 'delete_event': {
|
||||
try {
|
||||
const ids = args.ids as string[];
|
||||
const rundown = await deleteEntries(ids);
|
||||
return ok({ deleted: ids, order: rundown.order });
|
||||
} catch (e) {
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
|
||||
case 'reorder_event': {
|
||||
try {
|
||||
const { entryId, destinationId, order } = args as { entryId: string; destinationId: string; order: 'before' | 'after' | 'insert' };
|
||||
const rundown = await reorderEntry(entryId, destinationId, order);
|
||||
return ok({ order: rundown.order });
|
||||
} catch (e) {
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
|
||||
case 'list_rundowns': {
|
||||
return ok(rundownListResponse());
|
||||
}
|
||||
|
||||
case 'create_rundown': {
|
||||
try {
|
||||
const rundown = makeNewRundown();
|
||||
rundown.title = args.title as string;
|
||||
await getDataProvider().setRundown(rundown.id, rundown);
|
||||
return ok(rundownListResponse());
|
||||
} catch (e) {
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
|
||||
case 'load_rundown': {
|
||||
try {
|
||||
await loadRundown(args.id as string);
|
||||
return ok(rundownListResponse());
|
||||
} catch (e) {
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
|
||||
case 'rename_rundown': {
|
||||
try {
|
||||
const { id, title } = args as { id: string; title: string };
|
||||
const dataProvider = getDataProvider();
|
||||
const rundown = structuredClone(dataProvider.getRundown(id)) as { id: string; title: string };
|
||||
rundown.title = title;
|
||||
await dataProvider.setRundown(id, rundown as never);
|
||||
if (id === getCurrentRundown().id) {
|
||||
const customFields = dataProvider.getCustomFields();
|
||||
await initRundown(rundown as never, customFields);
|
||||
}
|
||||
return ok(rundownListResponse());
|
||||
} catch (e) {
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
|
||||
case 'delete_rundown': {
|
||||
try {
|
||||
const id = args.id as string;
|
||||
const dataProvider = getDataProvider();
|
||||
const currentId = getCurrentRundown().id;
|
||||
if (id === currentId) {
|
||||
return err('Cannot delete the currently loaded rundown');
|
||||
}
|
||||
const rundowns = dataProvider.getProjectRundowns();
|
||||
if (Object.keys(rundowns).length <= 1) {
|
||||
return err('Cannot delete the last rundown');
|
||||
}
|
||||
await dataProvider.deleteRundown(id);
|
||||
return ok(rundownListResponse());
|
||||
} catch (e) {
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
|
||||
case 'duplicate_rundown': {
|
||||
try {
|
||||
const id = args.id as string;
|
||||
const dataProvider = getDataProvider();
|
||||
const rundown = dataProvider.getRundown(id);
|
||||
const copy = duplicateRundown(rundown as never, `Copy of ${rundown.title}`);
|
||||
await dataProvider.setRundown(copy.id, copy);
|
||||
return ok(rundownListResponse());
|
||||
} catch (e) {
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
|
||||
case 'get_timer_state': {
|
||||
const state = getState();
|
||||
return ok({
|
||||
clock: state.clock,
|
||||
timer: state.timer,
|
||||
eventNow: state.eventNow,
|
||||
eventNext: state.eventNext,
|
||||
offset: state.offset,
|
||||
});
|
||||
}
|
||||
|
||||
case 'get_project_info': {
|
||||
return ok(getProjectData());
|
||||
}
|
||||
|
||||
case 'update_project_info': {
|
||||
try {
|
||||
const updated = await editCurrentProjectData(args as never);
|
||||
return ok(updated);
|
||||
} catch (e) {
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
|
||||
case 'list_automations': {
|
||||
const settings = getAutomationSettings();
|
||||
return ok({
|
||||
enabledAutomations: settings.enabledAutomations,
|
||||
triggers: settings.triggers,
|
||||
automations: settings.automations,
|
||||
});
|
||||
}
|
||||
|
||||
case 'create_automation': {
|
||||
try {
|
||||
const result = await addAutomation(args as never);
|
||||
return ok(result);
|
||||
} catch (e) {
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
|
||||
case 'get_custom_fields': {
|
||||
return ok(getProjectCustomFields());
|
||||
}
|
||||
|
||||
default:
|
||||
return err(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Build and configure a new MCP Server instance */
|
||||
function createMcpServer(): Server {
|
||||
const server = new Server(
|
||||
{ name: 'ontime', version: '1.0.0' },
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
prompts: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Handle tools/list
|
||||
server.setRequestHandler(ListToolsRequestSchema, async (): Promise<ListToolsResult> => {
|
||||
return { tools: TOOL_DEFINITIONS as unknown as ListToolsResult['tools'] };
|
||||
});
|
||||
|
||||
// Handle tools/call
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => {
|
||||
const { name, arguments: args = {} } = request.params;
|
||||
return handleToolCall(name, args as Record<string, unknown>);
|
||||
});
|
||||
|
||||
// Handle prompts/list
|
||||
server.setRequestHandler(ListPromptsRequestSchema, async (): Promise<ListPromptsResult> => {
|
||||
return {
|
||||
prompts: [
|
||||
{
|
||||
name: 'create_rundown_from_agenda',
|
||||
description: 'Generate MCP tool calls to build a rundown from a plain-text agenda',
|
||||
arguments: [
|
||||
{
|
||||
name: 'agenda',
|
||||
description: 'Plain-text agenda to convert into an Ontime rundown',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// Handle prompts/get
|
||||
server.setRequestHandler(GetPromptRequestSchema, async (request): Promise<GetPromptResult> => {
|
||||
const { name, arguments: args = {} } = request.params;
|
||||
if (name !== 'create_rundown_from_agenda') {
|
||||
throw new Error(`Unknown prompt: ${name}`);
|
||||
}
|
||||
const agenda = (args as Record<string, string>).agenda ?? '';
|
||||
return {
|
||||
description: 'Generate MCP tool calls to build a rundown from a plain-text agenda',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Convert the following agenda into an Ontime rundown using these rules:
|
||||
|
||||
- Times are in milliseconds from midnight. 09:00 = 32400000, 10:30 = 37800000, etc.
|
||||
- duration = timeEnd - timeStart
|
||||
- Cue prefixes by type: K01/K02/... for keynotes, P01/P02/... for panels, B01/B02/... for breaks
|
||||
- Colours by type: #4A90D9 for keynotes, #7B68EE for panels, #888888 for breaks, #E8A838 for meals
|
||||
- First call get_rundown to see the current state, then use create_event for each agenda item
|
||||
- Pass \`after: <previous event id>\` to chain events in sequence
|
||||
|
||||
Agenda:
|
||||
${agenda}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
/** Express router for the MCP endpoint */
|
||||
export const mcpRouter = express.Router();
|
||||
|
||||
// POST / — handle new or existing session
|
||||
mcpRouter.post('/', async (req, res) => {
|
||||
const body = req.body as unknown;
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
|
||||
if (isInitializeRequest(body)) {
|
||||
// New session: create server + transport
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
});
|
||||
|
||||
const id = randomUUID();
|
||||
sessions.set(id, transport);
|
||||
|
||||
transport.onclose = () => {
|
||||
sessions.delete(id);
|
||||
};
|
||||
|
||||
const mcpServer = createMcpServer();
|
||||
await mcpServer.connect(transport);
|
||||
await transport.handleRequest(req as never, res as never, body);
|
||||
return;
|
||||
}
|
||||
|
||||
// Existing session
|
||||
if (!sessionId || !sessions.has(sessionId)) {
|
||||
res.status(400).json({ error: 'Invalid or missing mcp-session-id' });
|
||||
return;
|
||||
}
|
||||
|
||||
const transport = sessions.get(sessionId)!;
|
||||
await transport.handleRequest(req as never, res as never, body);
|
||||
});
|
||||
|
||||
// GET / — SSE stream for existing session
|
||||
mcpRouter.get('/', async (req, res) => {
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
if (!sessionId || !sessions.has(sessionId)) {
|
||||
res.status(400).json({ error: 'Invalid or missing mcp-session-id' });
|
||||
return;
|
||||
}
|
||||
|
||||
const transport = sessions.get(sessionId)!;
|
||||
await transport.handleRequest(req as never, res as never);
|
||||
});
|
||||
|
||||
// DELETE / — close and remove session
|
||||
mcpRouter.delete('/', async (req, res) => {
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
if (!sessionId || !sessions.has(sessionId)) {
|
||||
res.status(400).json({ error: 'Invalid or missing mcp-session-id' });
|
||||
return;
|
||||
}
|
||||
|
||||
const transport = sessions.get(sessionId)!;
|
||||
sessions.delete(sessionId);
|
||||
await transport.close();
|
||||
res.status(200).json({ ok: true });
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import { socket } from './adapters/WebsocketAdapter.js';
|
||||
// Import Routers
|
||||
import { appRouter } from './api-data/index.js';
|
||||
import { integrationRouter } from './api-integration/integration.router.js';
|
||||
import { mcpRouter } from './api-mcp/mcp.router.js';
|
||||
import { flushPendingWrites, getDataProvider } from './classes/data-provider/DataProvider.js';
|
||||
// Services
|
||||
import { logger } from './classes/Logger.js';
|
||||
@@ -100,6 +101,7 @@ app.get(`${prefix}/ready`, (_req, res) => {
|
||||
app.use(`${prefix}/login`, loginRouter); // router for login flow
|
||||
app.use(`${prefix}/data`, authenticate, appRouter); // router for application data
|
||||
app.use(`${prefix}/api`, authenticate, integrationRouter); // router for integrations
|
||||
app.use(`${prefix}/mcp`, authenticate, mcpRouter); // router for MCP agent integration
|
||||
|
||||
// serve static external files
|
||||
app.use(
|
||||
|
||||
@@ -90,6 +90,14 @@ export function makeAuthenticateMiddleware(prefix: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader?.startsWith('Bearer ')) {
|
||||
const bearerToken = authHeader.slice(7);
|
||||
if (bearerToken === hashedPassword) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
res.status(401).send('Unauthorized');
|
||||
}
|
||||
|
||||
|
||||
Generated
+151
-1
@@ -224,6 +224,9 @@ importers:
|
||||
'@googleapis/sheets':
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: ^1.15.0
|
||||
version: 1.29.0(zod@4.4.3)
|
||||
cookie:
|
||||
specifier: 1.0.2
|
||||
version: 1.0.2
|
||||
@@ -1484,6 +1487,12 @@ packages:
|
||||
'@hapi/topo@5.1.0':
|
||||
resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==}
|
||||
|
||||
'@hono/node-server@1.19.14':
|
||||
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
peerDependencies:
|
||||
hono: ^4
|
||||
|
||||
'@isaacs/cliui@9.0.0':
|
||||
resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1539,6 +1548,16 @@ packages:
|
||||
'@marijn/find-cluster-break@1.0.2':
|
||||
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.29.0':
|
||||
resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@cfworker/json-schema': ^4.1.1
|
||||
zod: ^3.25 || ^4.0
|
||||
peerDependenciesMeta:
|
||||
'@cfworker/json-schema':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.1':
|
||||
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
||||
|
||||
@@ -2731,6 +2750,14 @@ packages:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ajv-formats@3.0.1:
|
||||
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
|
||||
peerDependencies:
|
||||
ajv: ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
ajv:
|
||||
optional: true
|
||||
|
||||
ajv-keywords@3.5.2:
|
||||
resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
|
||||
peerDependencies:
|
||||
@@ -2739,6 +2766,9 @@ packages:
|
||||
ajv@6.14.0:
|
||||
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
|
||||
|
||||
ajv@8.20.0:
|
||||
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3330,6 +3360,14 @@ packages:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
eventsource-parser@3.1.0:
|
||||
resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
eventsource@3.0.7:
|
||||
resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
expect-type@1.3.0:
|
||||
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -3337,6 +3375,12 @@ packages:
|
||||
exponential-backoff@3.1.3:
|
||||
resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==}
|
||||
|
||||
express-rate-limit@8.5.2:
|
||||
resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==}
|
||||
engines: {node: '>= 16'}
|
||||
peerDependencies:
|
||||
express: '>= 4.11'
|
||||
|
||||
express-static-gzip@3.0.1:
|
||||
resolution: {integrity: sha512-LMeU/3YjFlFUa4vrPX+RoMMRW5mIpF4Iysgs6gX7A59WCY4BzyF3O28mBr4eMlWuW4DU9wVAVuVcfx29ln1N6g==}
|
||||
|
||||
@@ -3370,6 +3414,9 @@ packages:
|
||||
fast-json-stable-stringify@2.1.0:
|
||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||
|
||||
fast-uri@3.1.2:
|
||||
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
|
||||
|
||||
fd-slicer@1.1.0:
|
||||
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
||||
|
||||
@@ -3574,6 +3621,10 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hono@4.12.27:
|
||||
resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
hookable@6.1.0:
|
||||
resolution: {integrity: sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==}
|
||||
|
||||
@@ -3642,6 +3693,10 @@ packages:
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
ip-address@10.2.0:
|
||||
resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -3711,6 +3766,9 @@ packages:
|
||||
joi@17.13.3:
|
||||
resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==}
|
||||
|
||||
jose@6.2.3:
|
||||
resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@@ -3735,6 +3793,12 @@ packages:
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
json-schema-typed@8.0.2:
|
||||
resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
|
||||
|
||||
json-stringify-safe@5.0.1:
|
||||
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
|
||||
|
||||
@@ -4166,6 +4230,10 @@ packages:
|
||||
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pkce-challenge@5.0.1:
|
||||
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
|
||||
playwright-core@1.60.0:
|
||||
resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -4340,6 +4408,10 @@ packages:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-main-filename@2.0.0:
|
||||
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||
|
||||
@@ -5085,6 +5157,14 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
zod-to-json-schema@3.25.2:
|
||||
resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==}
|
||||
peerDependencies:
|
||||
zod: ^3.25.28 || ^4
|
||||
|
||||
zod@4.4.3:
|
||||
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
|
||||
|
||||
zustand@5.0.14:
|
||||
resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
@@ -6266,6 +6346,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@hapi/hoek': 9.3.0
|
||||
|
||||
'@hono/node-server@1.19.14(hono@4.12.27)':
|
||||
dependencies:
|
||||
hono: 4.12.27
|
||||
|
||||
'@isaacs/cliui@9.0.0': {}
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
@@ -6331,6 +6415,28 @@ snapshots:
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2': {}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.14(hono@4.12.27)
|
||||
ajv: 8.20.0
|
||||
ajv-formats: 3.0.1(ajv@8.20.0)
|
||||
content-type: 1.0.5
|
||||
cors: 2.8.6
|
||||
cross-spawn: 7.0.6
|
||||
eventsource: 3.0.7
|
||||
eventsource-parser: 3.1.0
|
||||
express: 5.2.1
|
||||
express-rate-limit: 8.5.2(express@5.2.1)
|
||||
hono: 4.12.27
|
||||
jose: 6.2.3
|
||||
json-schema-typed: 8.0.2
|
||||
pkce-challenge: 5.0.1
|
||||
raw-body: 3.0.2
|
||||
zod: 4.4.3
|
||||
zod-to-json-schema: 3.25.2(zod@4.4.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.1':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.9.1
|
||||
@@ -7251,6 +7357,10 @@ snapshots:
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ajv-formats@3.0.1(ajv@8.20.0):
|
||||
optionalDependencies:
|
||||
ajv: 8.20.0
|
||||
|
||||
ajv-keywords@3.5.2(ajv@6.14.0):
|
||||
dependencies:
|
||||
ajv: 6.14.0
|
||||
@@ -7262,6 +7372,13 @@ snapshots:
|
||||
json-schema-traverse: 0.4.1
|
||||
uri-js: 4.4.1
|
||||
|
||||
ajv@8.20.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.2
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
@@ -7823,7 +7940,7 @@ snapshots:
|
||||
'@electron/asar': 3.4.1
|
||||
debug: 4.4.3
|
||||
fs-extra: 7.0.1
|
||||
lodash: 4.17.23
|
||||
lodash: 4.18.1
|
||||
temp: 0.9.4
|
||||
optionalDependencies:
|
||||
'@electron/windows-sign': 1.2.2
|
||||
@@ -7982,10 +8099,21 @@ snapshots:
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
eventsource-parser@3.1.0: {}
|
||||
|
||||
eventsource@3.0.7:
|
||||
dependencies:
|
||||
eventsource-parser: 3.1.0
|
||||
|
||||
expect-type@1.3.0: {}
|
||||
|
||||
exponential-backoff@3.1.3: {}
|
||||
|
||||
express-rate-limit@8.5.2(express@5.2.1):
|
||||
dependencies:
|
||||
express: 5.2.1
|
||||
ip-address: 10.2.0
|
||||
|
||||
express-static-gzip@3.0.1:
|
||||
dependencies:
|
||||
mime-types: 3.0.1
|
||||
@@ -8053,6 +8181,8 @@ snapshots:
|
||||
|
||||
fast-json-stable-stringify@2.1.0: {}
|
||||
|
||||
fast-uri@3.1.2: {}
|
||||
|
||||
fd-slicer@1.1.0:
|
||||
dependencies:
|
||||
pend: 1.2.0
|
||||
@@ -8322,6 +8452,8 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hono@4.12.27: {}
|
||||
|
||||
hookable@6.1.0: {}
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
@@ -8405,6 +8537,8 @@ snapshots:
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
ip-address@10.2.0: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
is-arrayish@0.2.1: {}
|
||||
@@ -8460,6 +8594,8 @@ snapshots:
|
||||
'@sideway/formula': 3.0.1
|
||||
'@sideway/pinpoint': 2.0.0
|
||||
|
||||
jose@6.2.3: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
@@ -8478,6 +8614,10 @@ snapshots:
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
json-schema-typed@8.0.2: {}
|
||||
|
||||
json-stringify-safe@5.0.1:
|
||||
optional: true
|
||||
|
||||
@@ -8867,6 +9007,8 @@ snapshots:
|
||||
|
||||
picomatch@4.0.4: {}
|
||||
|
||||
pkce-challenge@5.0.1: {}
|
||||
|
||||
playwright-core@1.60.0: {}
|
||||
|
||||
playwright@1.60.0:
|
||||
@@ -9027,6 +9169,8 @@ snapshots:
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
require-main-filename@2.0.0: {}
|
||||
|
||||
resedit@1.7.2:
|
||||
@@ -9833,6 +9977,12 @@ snapshots:
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod-to-json-schema@3.25.2(zod@4.4.3):
|
||||
dependencies:
|
||||
zod: 4.4.3
|
||||
|
||||
zod@4.4.3: {}
|
||||
|
||||
zustand@5.0.14(@types/react@19.1.12)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)):
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.12
|
||||
|
||||
Reference in New Issue
Block a user