← Files Biological Sequence & Alignment ViewerARCHIVED FILE
src/viewer-commands.ts
21.5 KB · Sep 30, 2026 · 23:01 UTC
import { z } from "zod";
import {
sequenceViewerOperationCommandSchema,
sequenceWorkbenchPanelGroupSchema,
} from "./viewer-operations";
export const SEQUENCE_VIEWER_CONTROL_TOOL_NAME = "sequence.control_viewer";
export const SEQUENCE_VIEWER_REGISTER_SESSION_TOOL_NAME =
"sequence.register_viewer_session";
export const SEQUENCE_VIEWER_WAIT_FOR_COMMAND_TOOL_NAME =
"sequence.wait_for_viewer_command";
export const SEQUENCE_VIEWER_COMPLETE_COMMAND_TOOL_NAME =
"sequence.complete_viewer_command";
const sequenceViewerActionSchema = z.enum([
"clear_alignment_selection",
"clear_read_selection",
"clear_sequence_selection",
"compute_alignment_guide_tree",
"dismiss_workbench_feedback",
"filter_alignment_rows",
"focus_alignment_cell",
"focus_alignment_reference_coordinate",
"focus_sequence_coordinate",
"navigate_alignment_search_hit",
"navigate_sequence_search_hit",
"navigate_sequence_feature",
"reset_alignment_view",
"search_alignment",
"search_sequence",
"select_alignment_columns",
"select_alignment_rows",
"select_read",
"select_sequence_feature",
"select_sequence_range",
"set_alignment_reference",
"set_alignment_row_visibility",
"set_alignment_view_options",
"set_chromatogram_view_options",
"set_display_mode",
"set_mode",
"set_quality_view_options",
"set_read_pileup_options",
"set_sequence_annotation_index",
"set_sequence_record",
"set_sequence_record_browser",
"set_sequence_view_options",
"set_toolbar_visibility",
"set_workbench_panel",
"set_workbench_disclosure",
"show_all_alignment_rows",
]);
const metricTrackSchema = z.enum([
"gap",
"identity",
"mismatch",
"modality-conservation",
"rna-structure",
"sequence-logo",
]);
const alignmentRowSortKeySchema = z.enum([
"coverage",
"identity",
"label",
"length",
"mismatches",
"source",
]);
const qualityTableSchema = z.enum([
"cycle-quality",
"cycle-composition",
"read-length",
"read-gc",
"read-mean-quality",
"repeated-sequences",
"frequent-kmers",
]);
function hasControlOption(command: Record<string, unknown>): boolean {
return Object.entries(command).some(
([key, value]) => key !== "action" && value !== undefined,
);
}
export const sequenceViewerControlCommandSchema = z.discriminatedUnion(
"action",
[
z.object({
action: z.literal("clear_alignment_selection"),
}),
z.object({ action: z.literal("clear_read_selection") }).strict(),
z.object({
action: z.literal("clear_sequence_selection"),
}),
z.object({
action: z.literal("compute_alignment_guide_tree"),
}),
z
.object({
action: z.literal("dismiss_workbench_feedback"),
feedbackId: z
.string()
.max(128)
.regex(/^(?:alignment|sequence)\.[a-z0-9][a-z0-9.-]*$/u),
})
.strict(),
z.object({
action: z.literal("filter_alignment_rows"),
query: z.string().max(500),
}),
z.object({
action: z.literal("focus_alignment_cell"),
column: z.number().int().positive(),
row: z.string().trim().min(1).max(500),
}),
z.object({
action: z.literal("focus_alignment_reference_coordinate"),
coordinate: z.number().int().positive(),
}),
z.object({
action: z.literal("focus_sequence_coordinate"),
coordinate: z.number().int().positive(),
record: z.string().trim().min(1).max(500).optional(),
}),
z.object({
action: z.literal("navigate_alignment_search_hit"),
direction: z.enum(["next", "previous"]),
}),
z.object({
action: z.literal("navigate_sequence_search_hit"),
direction: z.enum(["next", "previous"]),
}),
z
.object({
action: z.literal("navigate_sequence_feature"),
direction: z.enum(["next", "previous"]),
})
.strict(),
z.object({
action: z.literal("reset_alignment_view"),
}),
z.object({
action: z.literal("search_alignment"),
query: z.string().max(500),
}),
z.object({
action: z.literal("search_sequence"),
query: z.string().max(500),
record: z.string().trim().min(1).max(500).optional(),
}),
z.object({
action: z.literal("select_alignment_columns"),
end: z.number().int().positive(),
start: z.number().int().positive(),
}),
z
.object({
action: z.literal("select_alignment_rows"),
rows: z.array(z.string().trim().min(1).max(500)).max(500),
})
.strict(),
z
.object({
action: z.literal("select_read"),
sourceReadIndex: z
.number()
.int()
.nonnegative()
.describe(
"Zero-based index into the currently loaded track.reads array, not a SAM line or BAM/CRAM file ordinal. Use the identity returned by query_viewer reads.",
),
trackId: z.string().trim().min(1).max(500),
})
.strict(),
z.object({
action: z.literal("select_sequence_range"),
end: z.number().int().positive(),
record: z.string().trim().min(1).max(500).optional(),
start: z.number().int().positive(),
wraparound: z.boolean().optional(),
}),
z.object({
action: z.literal("select_sequence_feature"),
featureId: z
.string()
.trim()
.min(1)
.max(500)
.describe(
"Exact generated feature ID or a unique case-insensitive biological type, label, or qualifier value such as CDS or kinase domain.",
),
record: z.string().trim().min(1).max(500).optional(),
}),
z.object({
action: z.literal("set_alignment_row_visibility"),
rows: z.array(z.string().trim().min(1).max(500)).min(1).max(200),
visible: z.boolean(),
}),
z.object({
action: z.literal("set_alignment_reference"),
reference: z.string().trim().min(1).max(500),
}),
z
.object({
action: z.literal("set_alignment_view_options"),
analysisScope: z
.enum(["all-unhidden-rows", "currently-displayed-rows"])
.optional(),
cellWidth: z.number().int().min(16).max(42).optional(),
enabledMetricTracks: z.array(metricTrackSchema).max(6).optional(),
colorMode: z
.enum([
"coding-impact",
"difference",
"identity",
"nucleotide-substitution",
"protein-conservation",
"protein-similarity",
"residue",
])
.optional(),
moleculeType: z
.enum([
"dna",
"mixed",
"nucleic-acid-ambiguous",
"protein",
"rna",
"unknown",
])
.optional(),
residuePalette: z
.enum([
"clustal-x",
"hydrophobicity",
"jalview-nucleotide",
"muted-amino-acid",
"muted-nucleic-acid",
"ncbi-nucleic-acid",
"neutral",
"nucleotide-ambiguity",
"purine-pyrimidine",
"rasmol",
"zappo",
])
.optional(),
rowSortDirection: z.enum(["asc", "desc"]).optional(),
rowSortKey: alignmentRowSortKeySchema.optional(),
searchScope: z
.enum(["all-unhidden-rows", "currently-displayed-rows"])
.optional(),
showAnnotationTracks: z.boolean().optional(),
showIdenticalAsDots: z.boolean().optional(),
showRnaStructureOverlays: z.boolean().optional(),
showSequenceLogoHelp: z.boolean().optional(),
})
.refine(
({
analysisScope,
cellWidth,
colorMode,
enabledMetricTracks,
moleculeType,
residuePalette,
rowSortDirection,
rowSortKey,
searchScope,
showAnnotationTracks,
showIdenticalAsDots,
showRnaStructureOverlays,
showSequenceLogoHelp,
}) =>
[
analysisScope,
cellWidth,
colorMode,
enabledMetricTracks,
moleculeType,
residuePalette,
rowSortDirection,
rowSortKey,
searchScope,
showAnnotationTracks,
showIdenticalAsDots,
showRnaStructureOverlays,
showSequenceLogoHelp,
].some((value) => value != null),
"set_alignment_view_options requires at least one option.",
),
z
.object({
action: z.literal("set_chromatogram_view_options"),
basesPerWindow: z.number().int().min(1).max(100).optional(),
firstBase: z.number().int().positive().optional(),
})
.strict()
.refine(
hasControlOption,
"set_chromatogram_view_options requires at least one option.",
),
z.object({
action: z.literal("set_display_mode"),
displayMode: z.enum(["fullscreen", "inline"]),
}),
z.object({
action: z.literal("set_mode"),
mode: z.enum(["alignment", "sequence"]),
}),
z
.object({
action: z.literal("set_quality_view_options"),
distributionsExpanded: z.boolean().optional(),
expandedTables: z.array(qualityTableSchema).max(7).optional(),
methodsExpanded: z.boolean().optional(),
})
.strict()
.refine(
hasControlOption,
"set_quality_view_options requires at least one option.",
),
z
.object({
action: z.literal("set_read_pileup_options"),
includeDuplicates: z.boolean().optional(),
includeQcFailed: z.boolean().optional(),
includeSecondary: z.boolean().optional(),
includeSupplementary: z.boolean().optional(),
includeUnknownMappingQuality: z.boolean().optional(),
minimumMappingQuality: z.number().int().min(0).max(255).optional(),
showAllBases: z.boolean().optional(),
showSoftClips: z.boolean().optional(),
sortBy: z.enum(["position", "mapping-quality", "strand"]).optional(),
strand: z.enum(["all", "+", "-"]).optional(),
})
.strict()
.refine(
hasControlOption,
"set_read_pileup_options requires at least one option.",
),
z
.object({
action: z.literal("set_sequence_annotation_index"),
expanded: z.boolean().optional(),
page: z.number().int().nonnegative().optional(),
query: z.string().max(500).optional(),
})
.strict()
.refine(
hasControlOption,
"set_sequence_annotation_index requires at least one option.",
),
z.object({
action: z.literal("set_sequence_record"),
record: z.string().trim().min(1).max(500),
}),
z
.object({
action: z.literal("set_sequence_record_browser"),
expanded: z.boolean().optional(),
page: z.number().int().nonnegative().optional(),
query: z.string().max(500).optional(),
sortBy: z.enum(["source", "label", "length", "molecule"]).optional(),
})
.strict()
.refine(
hasControlOption,
"set_sequence_record_browser requires at least one option.",
),
z
.object({
action: z.literal("set_sequence_view_options"),
geneticCodeId: z.number().int().positive().optional(),
layout: z.enum(["circular", "linear", "split"]).optional(),
orientation: z.enum(["forward", "reverse-complement"]).optional(),
originRangeExpanded: z.boolean().optional(),
palette: z
.enum([
"clustal-x",
"hydrophobicity",
"jalview-nucleotide",
"muted-amino-acid",
"muted-nucleic-acid",
"ncbi-nucleic-acid",
"neutral",
"nucleotide-ambiguity",
"purine-pyrimidine",
"rasmol",
"zappo",
])
.optional(),
showFeatures: z.boolean().optional(),
showQuality: z.boolean().optional(),
showTranslation: z.boolean().optional(),
synchronizedViews: z.boolean().optional(),
wrapWidth: z
.union([z.literal(40), z.literal(50), z.literal(60), z.literal(80)])
.optional(),
})
.refine(
({
geneticCodeId,
layout,
orientation,
originRangeExpanded,
palette,
showFeatures,
showQuality,
showTranslation,
synchronizedViews,
wrapWidth,
}) =>
[
geneticCodeId,
layout,
orientation,
originRangeExpanded,
palette,
showFeatures,
showQuality,
showTranslation,
synchronizedViews,
wrapWidth,
].some((value) => value != null),
"set_sequence_view_options requires at least one option.",
),
z.object({
action: z.literal("set_toolbar_visibility"),
visible: z.boolean(),
}),
z
.object({
action: z.literal("set_workbench_panel"),
group: sequenceWorkbenchPanelGroupSchema,
panel: z.string().trim().min(1).max(100).nullable(),
})
.strict(),
z
.object({
action: z.literal("set_workbench_disclosure"),
disclosureId: z
.string()
.max(128)
.regex(/^(?:alignment|sequence)\.[a-z0-9][a-z0-9.-]*$/u),
expanded: z.boolean(),
})
.strict(),
z.object({
action: z.literal("show_all_alignment_rows"),
}),
],
);
export const sequenceViewerCommandSchema = z.union([
sequenceViewerControlCommandSchema,
sequenceViewerOperationCommandSchema,
]);
export const sequenceViewerControlInputSchema = z
.object({
sessionId: z
.string()
.uuid()
.describe("Active viewer session ID from the viewer's model context."),
})
.passthrough()
.transform((input, context) => {
const { sessionId, ...candidate } = input;
const parsed = sequenceViewerControlCommandSchema.safeParse(candidate);
if (!parsed.success) {
for (const issue of parsed.error.issues) {
context.addIssue({
code: "custom",
message: issue.message,
path: issue.path,
});
}
return z.NEVER;
}
return { ...parsed.data, sessionId };
});
// MCP tool registration accepts an object shape. Keep this discoverable shape
// broad, then apply the action-specific discriminated union in the handler.
export const sequenceViewerControlToolInputSchema = z.object({
action: sequenceViewerActionSchema,
analysisScope: z
.enum(["all-unhidden-rows", "currently-displayed-rows"])
.optional(),
cellWidth: z.number().int().min(16).max(42).optional(),
basesPerWindow: z.number().int().min(1).max(100).optional(),
colorMode: z
.enum([
"coding-impact",
"difference",
"identity",
"nucleotide-substitution",
"protein-conservation",
"protein-similarity",
"residue",
])
.optional(),
column: z.number().int().positive().optional(),
coordinate: z.number().int().positive().optional(),
direction: z.enum(["next", "previous"]).optional(),
displayMode: z.enum(["fullscreen", "inline"]).optional(),
disclosureId: z
.string()
.max(128)
.regex(/^(?:alignment|sequence)\.[a-z0-9][a-z0-9.-]*$/u)
.describe(
"Exact registered ID returned by query_viewer target workbench-disclosures; expanding reveals its parent tool and sections.",
)
.optional(),
distributionsExpanded: z.boolean().optional(),
end: z.number().int().positive().optional(),
enabledMetricTracks: z.array(metricTrackSchema).max(6).optional(),
expanded: z.boolean().optional(),
expandedTables: z.array(qualityTableSchema).max(7).optional(),
firstBase: z.number().int().positive().optional(),
feedbackId: z
.string()
.max(128)
.regex(/^(?:alignment|sequence)\.[a-z0-9][a-z0-9.-]*$/u)
.describe(
"Exact registered copy or session-error feedback ID from query target workbench-feedback. This cannot approve, cancel, or confirm source writes.",
)
.optional(),
featureId: z
.string()
.trim()
.min(1)
.max(500)
.describe(
"Exact generated feature ID or a unique case-insensitive biological type, label, or qualifier value such as CDS or kinase domain.",
)
.optional(),
geneticCodeId: z.number().int().positive().optional(),
group: sequenceWorkbenchPanelGroupSchema.optional(),
includeDuplicates: z.boolean().optional(),
includeQcFailed: z.boolean().optional(),
includeSecondary: z.boolean().optional(),
includeSupplementary: z.boolean().optional(),
includeUnknownMappingQuality: z.boolean().optional(),
layout: z.enum(["circular", "linear", "split"]).optional(),
mode: z.enum(["alignment", "sequence"]).optional(),
methodsExpanded: z.boolean().optional(),
minimumMappingQuality: z.number().int().min(0).max(255).optional(),
moleculeType: z
.enum([
"dna",
"mixed",
"nucleic-acid-ambiguous",
"protein",
"rna",
"unknown",
])
.optional(),
orientation: z.enum(["forward", "reverse-complement"]).optional(),
originRangeExpanded: z.boolean().optional(),
page: z.number().int().nonnegative().optional(),
panel: z.string().trim().min(1).max(100).nullable().optional(),
palette: z
.enum([
"clustal-x",
"hydrophobicity",
"jalview-nucleotide",
"muted-amino-acid",
"muted-nucleic-acid",
"ncbi-nucleic-acid",
"neutral",
"nucleotide-ambiguity",
"purine-pyrimidine",
"rasmol",
"zappo",
])
.describe(
"Single-sequence palette ID. Defaults are muted-nucleic-acid (Soft nucleotide) for DNA/RNA and muted-amino-acid (Soft amino acid) for proteins; neutral selects Monochrome. Query sequence-ui-state for the active record's palette options. Use residuePalette for alignments.",
)
.optional(),
query: z.string().max(500).optional(),
record: z.string().trim().min(1).max(500).optional(),
reference: z.string().trim().min(1).max(500).optional(),
residuePalette: z
.enum([
"clustal-x",
"hydrophobicity",
"jalview-nucleotide",
"muted-amino-acid",
"muted-nucleic-acid",
"ncbi-nucleic-acid",
"neutral",
"nucleotide-ambiguity",
"purine-pyrimidine",
"rasmol",
"zappo",
])
.describe(
"Alignment residue palette ID: muted-nucleic-acid for DNA/RNA, muted-amino-acid for proteins, or neutral for Monochrome. Existing scientific palettes remain available; the mounted viewer validates molecule compatibility.",
)
.optional(),
row: z
.string()
.trim()
.min(1)
.max(500)
.describe(
"Single alignment row ID or label; use only with focus_alignment_cell.",
)
.optional(),
rows: z
.array(z.string().trim().min(1).max(500))
.max(500)
.describe(
"Alignment row IDs or unique labels; required with set_alignment_row_visibility (1–200 rows), even for one row. select_alignment_rows accepts 0–500, where an empty list clears the selection.",
)
.optional(),
rowSortDirection: z.enum(["asc", "desc"]).optional(),
rowSortKey: alignmentRowSortKeySchema.optional(),
searchScope: z
.enum(["all-unhidden-rows", "currently-displayed-rows"])
.optional(),
sessionId: z
.string()
.uuid()
.describe("Active viewer session ID returned by sequence.open_from_chat."),
showAnnotationTracks: z.boolean().optional(),
showAllBases: z.boolean().optional(),
showFeatures: z.boolean().optional(),
showIdenticalAsDots: z.boolean().optional(),
showQuality: z.boolean().optional(),
showRnaStructureOverlays: z.boolean().optional(),
showSequenceLogoHelp: z.boolean().optional(),
showSoftClips: z.boolean().optional(),
showTranslation: z.boolean().optional(),
start: z.number().int().positive().optional(),
sortBy: z
.enum([
"source",
"label",
"length",
"molecule",
"position",
"mapping-quality",
"strand",
])
.optional(),
sourceReadIndex: z
.number()
.int()
.nonnegative()
.describe(
"Zero-based index into the currently materialized loaded track.reads array; not a file ordinal, line number, or byte offset.",
)
.optional(),
strand: z.enum(["all", "+", "-"]).optional(),
synchronizedViews: z.boolean().optional(),
visible: z.boolean().optional(),
trackId: z.string().trim().min(1).max(500).optional(),
wrapWidth: z
.union([z.literal(40), z.literal(50), z.literal(60), z.literal(80)])
.optional(),
wraparound: z
.boolean()
.describe(
"For select_sequence_range on a circular record, follow start through the origin to end. Start must be greater than end.",
)
.optional(),
});
export type SequenceViewerCommand = z.infer<typeof sequenceViewerCommandSchema>;
export type SequenceViewerControlCommand = z.infer<
typeof sequenceViewerControlCommandSchema
>;
export type SequenceViewerCommandAction = SequenceViewerCommand["action"];
export type QueuedSequenceViewerCommand = SequenceViewerCommand & {
commandId: string;
revision: number;
};
export const queuedSequenceViewerCommandSchema: z.ZodType<QueuedSequenceViewerCommand> =
z
.object({
action: z.string(),
commandId: z.string().uuid(),
revision: z.number().int().positive(),
})
.passthrough()
.transform((value, context) => {
const { commandId, revision, ...candidate } = value;
const parsed = sequenceViewerCommandSchema.safeParse(candidate);
if (!parsed.success) {
context.addIssue({
code: "custom",
message: parsed.error.issues.map(({ message }) => message).join("; "),
});
return z.NEVER;
}
return { ...parsed.data, commandId, revision };
});
export type SequenceViewerCommandResult = {
applied: boolean;
message: string;
state?: Record<string, unknown>;
};
SHA-256: 787ab51bc1fc762db2561a465e56b17bf846963060a072419c844333cec42cbe