feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
This commit is contained in:
Executable
+29
@@ -0,0 +1,29 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export type AuditOutcome = "started" | "succeeded" | "denied" | "failed";
|
||||
|
||||
export interface AuditEvent {
|
||||
operationId: string;
|
||||
action: string;
|
||||
outcome: AuditOutcome;
|
||||
timestamp: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type AuditSink = (event: AuditEvent) => void;
|
||||
|
||||
export function createOperationId(): string {
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
/** JSONL on stderr keeps the stdio protocol clean and gives operators correlation IDs. */
|
||||
export const stderrAuditSink: AuditSink = (event) => {
|
||||
console.error(JSON.stringify({ type: "premiere-mcp-audit", ...event }));
|
||||
};
|
||||
|
||||
export function emitAudit(
|
||||
sink: AuditSink,
|
||||
event: Omit<AuditEvent, "timestamp">,
|
||||
): void {
|
||||
sink({ ...event, timestamp: new Date().toISOString() });
|
||||
}
|
||||
Executable
+374
@@ -0,0 +1,374 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export const CAPABILITIES = ["inspect", "edit", "export", "filesystem", "unsafe-script"] as const;
|
||||
|
||||
export type Capability = (typeof CAPABILITIES)[number];
|
||||
|
||||
export interface CapabilityConfig {
|
||||
capabilities: ReadonlySet<Capability>;
|
||||
source: "default" | "environment" | "explicit";
|
||||
}
|
||||
|
||||
const KNOWN = new Set<string>(CAPABILITIES);
|
||||
|
||||
/**
|
||||
* Resolve the server's authority. Unsafe scripting is deliberately never part
|
||||
* of the default: an operator must name it explicitly.
|
||||
*/
|
||||
export function resolveCapabilities(
|
||||
value: string | undefined = process.env.PREMIERE_MCP_CAPABILITIES,
|
||||
): CapabilityConfig {
|
||||
if (value === undefined || value.trim() === "") {
|
||||
return { capabilities: new Set<Capability>(["inspect", "edit", "export", "filesystem"]), source: "default" };
|
||||
}
|
||||
|
||||
const capabilities = new Set<Capability>();
|
||||
for (const raw of value.split(",")) {
|
||||
const name = raw.trim().toLowerCase();
|
||||
if (!name) continue;
|
||||
if (!KNOWN.has(name)) {
|
||||
throw new Error(`Unknown Premiere MCP capability: ${name}`);
|
||||
}
|
||||
capabilities.add(name as Capability);
|
||||
}
|
||||
return { capabilities, source: "environment" };
|
||||
}
|
||||
|
||||
export class CapabilityDeniedError extends Error {
|
||||
readonly code = "CAPABILITY_DENIED";
|
||||
|
||||
constructor(readonly capability: Capability, readonly operationId: string) {
|
||||
super(`Operation ${operationId} requires the '${capability}' capability`);
|
||||
this.name = "CapabilityDeniedError";
|
||||
}
|
||||
}
|
||||
|
||||
export function requireCapability(
|
||||
config: CapabilityConfig,
|
||||
capability: Capability,
|
||||
operationId: string,
|
||||
): void {
|
||||
if (!config.capabilities.has(capability)) {
|
||||
throw new CapabilityDeniedError(capability, operationId);
|
||||
}
|
||||
}
|
||||
|
||||
export const UNSAFE_TOOL_NAMES = new Set(["execute_extendscript", "send_raw_script", "evaluate_expression"]);
|
||||
|
||||
const INSPECT_TOOL_NAMES = new Set([
|
||||
"ping",
|
||||
"get_capabilities",
|
||||
"preview_edit_plan",
|
||||
"preview_transcript_edit_uxp",
|
||||
"plan_transcript_rough_cut_uxp",
|
||||
"create_context_edit_plan",
|
||||
"create_editorial_context_pack",
|
||||
"create_editorial_plan",
|
||||
"preview_editorial_plan",
|
||||
"preview_project_intake",
|
||||
"preview_motion_graphics_demo",
|
||||
"preview_product_spot",
|
||||
"preview_brand_spot",
|
||||
"inspect_sequence_timing_uxp",
|
||||
"inspect_sequence_timing_by_guid_uxp",
|
||||
"inspect_frame_alignment_uxp",
|
||||
"calculate_tick_time_uxp",
|
||||
"validate_project_for_export",
|
||||
"read_sequence_captions",
|
||||
"plan_silence_review_markers",
|
||||
"analyze_dialogue_edit_candidates",
|
||||
"preview_derived_dialogue_sequence_uxp",
|
||||
]);
|
||||
// detect_silence reads a media file from disk and shells out to ffmpeg. It
|
||||
// changes nothing in Premiere, so classifying it as "edit" would overstate what
|
||||
// it does; filesystem is the authority it actually needs.
|
||||
const FILESYSTEM_TOOL_NAMES = new Set([
|
||||
"preview_mogrt_recipe",
|
||||
"verify_mogrt_artifact",
|
||||
"apply_lut",
|
||||
"set_scratch_disk_path",
|
||||
"verify_delivery_file",
|
||||
"verify_delivery_conformance",
|
||||
"detect_silence",
|
||||
"detect_beats",
|
||||
"create_project_backup",
|
||||
"analyze_loudness",
|
||||
"analyze_video_qc",
|
||||
"detect_source_scene_changes",
|
||||
"normalize_loudness_file",
|
||||
"inspect_media_streams",
|
||||
"generate_media_contact_sheet",
|
||||
"detect_audio_transients",
|
||||
"detect_motion_peaks",
|
||||
"read_video_scopes",
|
||||
"plan_shot_match",
|
||||
"analyze_video_interlacing",
|
||||
"detect_active_picture_bounds",
|
||||
"inspect_cmx3600_edl",
|
||||
"validate_cmx3600_edl",
|
||||
"compare_cmx3600_edls",
|
||||
"inspect_fcpxml_interchange",
|
||||
"verify_fcpxml_media_references",
|
||||
"search_workflow_recipes",
|
||||
"preview_workflow_recipe",
|
||||
"manage_media_watch",
|
||||
"preview_watched_media_import",
|
||||
]);
|
||||
|
||||
// These tools have deliberately mixed authority requirements that cannot be
|
||||
// inferred safely from their names. Keep them explicit: a preview reads an
|
||||
// approved folder, authoring mutates a saved AE project and exports a file.
|
||||
const TOOL_CAPABILITY_REQUIREMENTS: Readonly<Record<string, readonly Capability[]>> = {
|
||||
verify_after_effects_connection: ["inspect"],
|
||||
preview_mogrt_recipe: ["inspect", "filesystem"],
|
||||
create_mogrt_recipe: ["edit", "export", "filesystem"],
|
||||
verify_mogrt_artifact: ["inspect", "filesystem"],
|
||||
validate_mogrt_brand_kit: ["inspect", "filesystem"],
|
||||
preview_mogrt_batch: ["inspect", "filesystem"],
|
||||
create_mogrt_batch: ["edit", "export", "filesystem"],
|
||||
inspect_after_effects_template_source: ["inspect"],
|
||||
preview_mogrt_library_publish: ["inspect", "filesystem"],
|
||||
publish_mogrt_to_library: ["filesystem"],
|
||||
inspect_mogrt_library: ["inspect", "filesystem"],
|
||||
inspect_after_effects_render_templates: ["inspect"],
|
||||
preview_after_effects_render: ["inspect", "filesystem"],
|
||||
enqueue_after_effects_render: ["edit", "export", "filesystem"],
|
||||
preview_mogrt_premiere_handoff: ["inspect", "filesystem"],
|
||||
apply_mogrt_premiere_handoff: ["edit", "filesystem"],
|
||||
};
|
||||
|
||||
const ACTION_CAPABILITIES: Readonly<Record<string, Readonly<Record<string, readonly Capability[]>>>> = {
|
||||
manage_project_context: {
|
||||
capture: ["inspect", "filesystem"],
|
||||
enrich: ["inspect", "filesystem"],
|
||||
status: ["inspect"],
|
||||
clear: ["filesystem"],
|
||||
},
|
||||
manage_clip_effects_uxp: {
|
||||
catalog: ["inspect"],
|
||||
inspect: ["inspect"],
|
||||
add: ["edit"],
|
||||
remove: ["edit"],
|
||||
},
|
||||
batch_selected_clips_uxp: {
|
||||
inspect: ["inspect"],
|
||||
add_effect: ["edit"],
|
||||
remove_effect: ["edit"],
|
||||
},
|
||||
manage_timeline_selection_uxp: {
|
||||
inspect: ["inspect"],
|
||||
inspect_targets: ["inspect"],
|
||||
replace: ["edit"],
|
||||
add: ["edit"],
|
||||
remove: ["edit"],
|
||||
clear: ["edit"],
|
||||
},
|
||||
manage_proxy_ingest_uxp: {
|
||||
inspect_proxy: ["inspect"],
|
||||
attach_proxy: ["edit", "filesystem"],
|
||||
get_ingest: ["inspect"],
|
||||
set_ingest: ["edit"],
|
||||
},
|
||||
manage_metadata_uxp: {
|
||||
get: ["inspect"],
|
||||
update: ["edit"],
|
||||
},
|
||||
inspect_project_panel_metadata_uxp: {
|
||||
panel: ["inspect"],
|
||||
item_columns: ["inspect"],
|
||||
},
|
||||
manage_project_panel_metadata_uxp: {
|
||||
inspect: ["inspect"],
|
||||
update: ["edit"],
|
||||
},
|
||||
create_project_metadata_field_uxp: {
|
||||
inspect: ["inspect"],
|
||||
create: ["edit"],
|
||||
},
|
||||
manage_color_conformance_uxp: {
|
||||
preflight: ["inspect"],
|
||||
update: ["edit"],
|
||||
},
|
||||
audition_source_monitor_uxp: {
|
||||
state: ["inspect"],
|
||||
open_project_item: ["edit"],
|
||||
open_file: ["edit", "filesystem"],
|
||||
set_position: ["edit"],
|
||||
play: ["edit"],
|
||||
close: ["edit"],
|
||||
close_all: ["edit"],
|
||||
},
|
||||
preflight_production_storage_uxp: {
|
||||
preflight: ["inspect"],
|
||||
configure_project: ["edit"],
|
||||
},
|
||||
inspect_project_selection_uxp: {
|
||||
views: ["inspect"],
|
||||
selection: ["inspect"],
|
||||
},
|
||||
manage_markers_uxp: {
|
||||
inspect: ["inspect"],
|
||||
add: ["edit"],
|
||||
update: ["edit"],
|
||||
remove: ["edit"],
|
||||
remove_many: ["edit"],
|
||||
},
|
||||
organize_project_items_uxp: {
|
||||
inspect_bin: ["inspect"],
|
||||
create_bin: ["edit"],
|
||||
create_smart_bin: ["edit"],
|
||||
rename: ["edit"],
|
||||
move: ["edit"],
|
||||
set_color: ["edit"],
|
||||
remove: ["edit"],
|
||||
},
|
||||
manage_sequence_settings_uxp: {
|
||||
get: ["inspect"],
|
||||
update: ["edit"],
|
||||
},
|
||||
manage_sequence_display_format_uxp: {
|
||||
inspect: ["inspect"],
|
||||
update: ["edit"],
|
||||
},
|
||||
manage_sequence_range_uxp: {
|
||||
inspect: ["inspect"],
|
||||
update: ["edit"],
|
||||
},
|
||||
manage_sequence_playhead_uxp: {
|
||||
inspect: ["inspect"],
|
||||
set: ["edit"],
|
||||
},
|
||||
manage_app_preferences_uxp: {
|
||||
inspect: ["inspect"],
|
||||
set: ["edit"],
|
||||
},
|
||||
manage_timeline_source_label_uxp: {
|
||||
inspect: ["inspect"],
|
||||
update: ["edit"],
|
||||
},
|
||||
import_project_media_uxp: {
|
||||
files: ["edit", "filesystem"],
|
||||
sequences: ["edit", "filesystem"],
|
||||
ae_comps: ["edit", "filesystem"],
|
||||
all_ae_comps: ["edit", "filesystem"],
|
||||
},
|
||||
automate_effect_parameters_uxp: {
|
||||
inspect: ["inspect"],
|
||||
inspect_point_value: ["inspect"],
|
||||
inspect_point_displacement: ["inspect"],
|
||||
inspect_color_value: ["inspect"],
|
||||
inspect_keyframe: ["inspect"],
|
||||
inspect_time_varying: ["inspect"],
|
||||
set_point_value: ["edit"],
|
||||
set_color_value: ["edit"],
|
||||
set_value: ["edit"],
|
||||
add_keyframe: ["edit"],
|
||||
remove_keyframe: ["edit"],
|
||||
remove_keyframe_range: ["edit"],
|
||||
set_interpolation: ["edit"],
|
||||
set_time_varying: ["edit"],
|
||||
},
|
||||
transform_track_item_uxp: {
|
||||
inspect: ["inspect"],
|
||||
update: ["edit"],
|
||||
},
|
||||
edit_timeline_uxp: {
|
||||
insert: ["edit"],
|
||||
overwrite: ["edit"],
|
||||
clone_selection: ["edit"],
|
||||
remove_selection: ["edit"],
|
||||
insert_mogrt_path: ["edit", "filesystem"],
|
||||
insert_mogrt_library: ["edit"],
|
||||
},
|
||||
manage_sequences_uxp: {
|
||||
inspect: ["inspect"],
|
||||
create_from_media: ["edit"],
|
||||
clone: ["edit"],
|
||||
subsequence: ["edit"],
|
||||
activate: ["edit"],
|
||||
open: ["edit"],
|
||||
close: ["edit"],
|
||||
delete: ["edit"],
|
||||
},
|
||||
encode_media_uxp: {
|
||||
preflight: ["inspect"],
|
||||
sequence: ["export", "filesystem"],
|
||||
project_item: ["export", "filesystem"],
|
||||
file: ["export", "filesystem"],
|
||||
},
|
||||
};
|
||||
|
||||
/** A conservative classification for centralized server registration. */
|
||||
export function capabilityForTool(toolName: string): Capability {
|
||||
if (UNSAFE_TOOL_NAMES.has(toolName)) return "unsafe-script";
|
||||
if (/^(export_|validate_export_|start_batch_encode|queue_|encode_|capture_frame)/.test(toolName)) return "export";
|
||||
if (/^(import_|relink_|create_project|open_project|save_|consolidate_)/.test(toolName)) return "filesystem";
|
||||
if (FILESYSTEM_TOOL_NAMES.has(toolName)) return "filesystem";
|
||||
if (INSPECT_TOOL_NAMES.has(toolName) || /^(get_|list_|inspect_|find_|check_|search_)/.test(toolName)) return "inspect";
|
||||
// Every remaining registered tool changes Premiere state. Defaulting to edit
|
||||
// keeps new tools fail-closed instead of silently bypassing the authority profile.
|
||||
return "edit";
|
||||
}
|
||||
|
||||
/** Resolve authority at the action level for consolidated multi-action tools. */
|
||||
export function capabilitiesForToolInvocation(toolName: string, args: unknown): readonly Capability[] {
|
||||
const actionMap = ACTION_CAPABILITIES[toolName];
|
||||
if (!actionMap) return TOOL_CAPABILITY_REQUIREMENTS[toolName] ?? [capabilityForTool(toolName)];
|
||||
const input = args && typeof args === "object" && !Array.isArray(args)
|
||||
? args as Record<string, unknown>
|
||||
: undefined;
|
||||
const action = input?.action;
|
||||
const required = typeof action === "string" ? actionMap[action] : undefined;
|
||||
if (toolName === "encode_media_uxp" && action === "preflight" && typeof input?.preset_file === "string") {
|
||||
return ["inspect", "filesystem"];
|
||||
}
|
||||
return required
|
||||
? required
|
||||
: [capabilityForTool(toolName)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tools that stay advertised under every profile, because they are how an
|
||||
* operator diagnoses a profile that is too narrow: get_capabilities reports the
|
||||
* active authority, and ping proves the bridge is alive. Withholding them makes
|
||||
* a misconfigured server look broken with no supported way to ask why.
|
||||
*/
|
||||
export const ALWAYS_LISTED_TOOL_NAMES = new Set(["ping", "get_capabilities"]);
|
||||
|
||||
/**
|
||||
* Whether a tool should appear in tools/list under the given profile.
|
||||
*
|
||||
* This is an advertising decision, not an enforcement one — guardToolHandler
|
||||
* remains the authority check at call time. Keeping the two separate means a
|
||||
* listing bug can never widen what a profile is actually allowed to do.
|
||||
*/
|
||||
export function isToolPermitted(
|
||||
toolName: string,
|
||||
config: CapabilityConfig,
|
||||
): boolean {
|
||||
if (ALWAYS_LISTED_TOOL_NAMES.has(toolName)) return true;
|
||||
const exactRequirements = TOOL_CAPABILITY_REQUIREMENTS[toolName];
|
||||
if (exactRequirements) {
|
||||
return exactRequirements.every((capability) => config.capabilities.has(capability));
|
||||
}
|
||||
const actionMap = ACTION_CAPABILITIES[toolName];
|
||||
if (actionMap) {
|
||||
return Object.values(actionMap).some((required) => required.every((capability) => config.capabilities.has(capability)));
|
||||
}
|
||||
return config.capabilities.has(capabilityForTool(toolName));
|
||||
}
|
||||
|
||||
export function guardToolHandler<TArgs, TResult>(
|
||||
toolName: string,
|
||||
handler: (args: TArgs) => Promise<TResult>,
|
||||
config: CapabilityConfig = resolveCapabilities(),
|
||||
createOperationId: () => string = randomUUID,
|
||||
): (args: TArgs) => Promise<TResult> {
|
||||
return async (args: TArgs) => {
|
||||
const operationId = createOperationId();
|
||||
for (const required of capabilitiesForToolInvocation(toolName, args)) {
|
||||
requireCapability(config, required, operationId);
|
||||
}
|
||||
return handler(args);
|
||||
};
|
||||
}
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
export * from "./audit.js";
|
||||
export * from "./capabilities.js";
|
||||
Reference in New Issue
Block a user