# Directory Structure
```
├── .github
│ ├── copilot-instructions.md
│ └── workflows
│ ├── copilot-setup-steps.yml
│ └── test.yml
├── .gitignore
├── CLAUDE.md
├── Dockerfile
├── index.ts
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── screenshot.png
├── shortcuts.test.ts
├── shortcuts.ts
├── sse.ts
├── tsconfig.json
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
node_modules
dist
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Siri Shortcuts MCP Server
This MCP server provides access to Siri shortcuts functionality via the Model Context Protocol (MCP). It allows listing, opening, and running shortcuts from the macOS Shortcuts app.

## Features
- Exposes _all_ shortcuts, meaning the LLM can call anything that is available in the Shortcuts app.
- List all available shortcuts
- Open shortcuts in the Shortcuts app
- Run shortcuts with optional input parameters
- Dynamically generated tools for each available shortcut
## Tools
### Base Tools
1. `list_shortcuts`
- Lists all available Siri shortcuts on the system
- No input required
- Returns: Array of shortcut names
```json
{
"shortcuts": [{ "name": "My Shortcut 1" }, { "name": "My Shortcut 2" }]
}
```
2. `open_shortcut`
- Opens a shortcut in the Shortcuts app
- Input:
- `name` (string): Name of the shortcut to open
3. `run_shortcut`
- Runs a shortcut with optional input
- Input:
- `name` (string): Name or identifier (UUID) of the shortcut to run
- `input` (string, optional): Text input or filepath to pass to the shortcut
### Dynamic Tools
The server automatically generates additional tools for each available shortcut in the format:
- Tool name: `run_shortcut_[sanitized_shortcut_name]`
- Description: Runs the specific shortcut
- Input:
- `input` (string, optional): Text input or filepath to pass to the shortcut
## Configuration
The server supports the following environment variables:
- `GENERATE_SHORTCUT_TOOLS` (default: `true`): When set to `false`, disables the generation of dynamic shortcut tools. Only the base tools (`list_shortcuts`, `open_shortcut`, `run_shortcut`) will be available.
- `INJECT_SHORTCUT_LIST` (default: `false`): When set to `true`, injects the list of available shortcuts into the `run_shortcut` tool description to help the LLM understand which shortcuts are available.
## Usage with Claude
Add to your Claude configuration:
```json
{
"mcpServers": {
"siri-shortcuts": {
"command": "npx",
"args": ["mcp-server-siri-shortcuts"],
"env": {
"GENERATE_SHORTCUT_TOOLS": "true",
"INJECT_SHORTCUT_LIST": "false"
}
}
}
}
```
## Implementation Details
- Uses the macOS `shortcuts` CLI command under the hood
- Sanitizes shortcut names for tool naming compatibility
- Supports both direct text input and file-based input
- Returns shortcut output when available
- Implements standard MCP error handling
```
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
```markdown
.github/copilot-instructions.md
```
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
```typescript
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});
```
--------------------------------------------------------------------------------
/.github/copilot-instructions.md:
--------------------------------------------------------------------------------
```markdown
# Copilot Instructions
- This project uses [Vitest](https://vitest.dev/) for testing.
- All changes must pass `npm test` before being considered complete.
- All changes must pass `npm run build`
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"outDir": "./dist",
"rootDir": ".",
"skipLibCheck": true
},
"include": [
"./**/*.ts"
]
}
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
FROM node:22.12-alpine AS builder
COPY src/everything /app
COPY tsconfig.json /tsconfig.json
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm npm install
FROM node:22-alpine AS release
WORKDIR /app
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/package-lock.json /app/package-lock.json
ENV NODE_ENV=production
RUN npm ci --ignore-scripts --omit-dev
CMD ["node", "dist/index.js"]
```
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createServer } from "./shortcuts.js";
async function main() {
const transport = new StdioServerTransport();
const { server, cleanup } = createServer();
await server.connect(transport);
// Cleanup on exit
process.on("SIGINT", async () => {
await cleanup();
await server.close();
process.exit(0);
});
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
```yaml
name: Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
```
--------------------------------------------------------------------------------
/.github/workflows/copilot-setup-steps.yml:
--------------------------------------------------------------------------------
```yaml
name: "Copilot Setup Steps"
on: workflow_dispatch
jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "23"
cache: "npm"
- name: Cache node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-modules-
- name: Install dependencies
run: npm ci
```
--------------------------------------------------------------------------------
/sse.ts:
--------------------------------------------------------------------------------
```typescript
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import { createServer } from "./shortcuts.js";
const app = express();
const { server, cleanup } = createServer();
let transport: SSEServerTransport;
app.get("/sse", async (req, res) => {
console.log("Received connection");
transport = new SSEServerTransport("/message", res);
await server.connect(transport);
server.onclose = async () => {
await cleanup();
await server.close();
process.exit(0);
};
});
app.post("/message", async (req, res) => {
console.log("Received message");
await transport.handlePostMessage(req, res);
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-server-siri-shortcuts",
"version": "1.1.0",
"description": "MCP server that provides access to Siri shortcuts",
"license": "GPLv3",
"author": "David Mohl <[email protected]>",
"homepage": "https://github.com/dvcrn/mcp-server-siri-shortcuts",
"repository": {
"type": "git",
"url": "git+https://github.com/dvcrn/mcp-server-siri-shortcuts.git"
},
"bugs": "https://github.com/dvcrn/mcp-server-siri-shortcuts/issues",
"type": "module",
"bin": {
"mcp-server-siri-shortcuts": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch",
"start": "node dist/index.js",
"start:sse": "node dist/sse.js",
"test": "vitest"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.0.1",
"express": "^4.21.1",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.23.5"
},
"devDependencies": {
"@types/express": "^5.0.0",
"shx": "^0.3.4",
"typescript": "^5.6.2",
"vitest": "^3.2.0"
}
}
```
--------------------------------------------------------------------------------
/shortcuts.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ErrorCode,
ListResourcesRequestSchema,
ListPromptsRequestSchema,
McpError,
Tool,
ToolSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { exec, spawn } from "child_process";
import { zodToJsonSchema } from "zod-to-json-schema";
import path from "path";
import fs from "fs";
// Configuration from environment variables
const GENERATE_SHORTCUT_TOOLS = process.env.GENERATE_SHORTCUT_TOOLS !== "false";
const INJECT_SHORTCUT_LIST = process.env.INJECT_SHORTCUT_LIST === "true";
const ToolInputSchema = ToolSchema.shape.inputSchema;
type ToolInput = z.infer<typeof ToolInputSchema>;
/* Input schemas for tools implemented in this server */
const ListShortcutsSchema = z.object({}).strict();
const OpenShortcutSchema = z
.object({
name: z.string().describe("The name of the shortcut to open"),
})
.strict();
const RunShortcutSchema = z
.object({
name: z.string().describe("The name or identifier (UUID) of the shortcut to run"),
input: z
.string()
.optional()
.describe(
"The input to pass to the shortcut. Can be text, or a filepath",
),
})
.strict();
enum ToolName {
LIST_SHORTCUTS = "list_shortcuts",
OPEN_SHORTCUT = "open_shortcut",
RUN_SHORTCUT = "run_shortcut",
}
type OpenShortcutInput = z.infer<typeof OpenShortcutSchema>;
type RunShortcutInput = z.infer<typeof RunShortcutSchema>;
// Map to store shortcut names and their sanitized IDs
const shortcutMap = new Map<string, string>();
// Map to store shortcut names and their identifiers (UUIDs)
const shortcutIdentifierMap = new Map<string, string>();
// Helper function to generate unique sanitized names to avoid conflicts
export const generateUniqueSanitizedName = (originalName: string, existingSanitizedNames: Set<string>): string => {
let baseSanitized = sanitizeShortcutName(originalName);
let uniqueSanitized = baseSanitized;
let counter = 1;
// Check if this sanitized name already exists, if so add a counter
while (existingSanitizedNames.has(uniqueSanitized)) {
const suffix = `_${counter}`;
const maxLength = 64 - "run_shortcut_".length;
// Ensure the base name + suffix doesn't exceed the limit
if (baseSanitized.length + suffix.length > maxLength) {
const truncatedBase = baseSanitized.substring(0, maxLength - suffix.length);
uniqueSanitized = truncatedBase + suffix;
} else {
uniqueSanitized = baseSanitized + suffix;
}
counter++;
}
return uniqueSanitized;
};
type ToolResult = { [key: string]: any };
// Function to execute the list_shortcuts tool
const listShortcuts = async (): Promise<ToolResult> => {
return new Promise((resolve, reject) => {
exec("shortcuts list --show-identifiers", (error, stdout, stderr) => {
if (error) {
reject(
new McpError(
ErrorCode.InternalError,
`Failed to list shortcuts: ${error.message}`,
),
);
return;
}
if (stderr) {
reject(
new McpError(
ErrorCode.InternalError,
`Error listing shortcuts: ${stderr}`,
),
);
return;
}
// Parse output with identifiers format: "Name (UUID)"
const shortcuts = stdout
.split("\n")
.filter((line) => line.trim())
.map((line) => {
const trimmed = line.trim();
// Extract name and identifier if present
const match = trimmed.match(/^(.+?)\s*\(([A-F0-9-]+)\)$/);
if (match) {
return {
name: match[1].trim(),
identifier: match[2]
};
}
return { name: trimmed };
});
// Update the shortcut map with unique sanitized names
const existingSanitizedNames = new Set<string>();
shortcuts.forEach((shortcut) => {
const uniqueSanitizedName = generateUniqueSanitizedName(shortcut.name, existingSanitizedNames);
shortcutMap.set(shortcut.name, uniqueSanitizedName);
existingSanitizedNames.add(uniqueSanitizedName);
// Store identifier if present
if ('identifier' in shortcut && shortcut.identifier) {
shortcutIdentifierMap.set(shortcut.name, shortcut.identifier);
}
});
resolve({ shortcuts });
});
});
};
// Function to execute the open_shortcut tool
const openShortcut = async (params: OpenShortcutInput): Promise<ToolResult> => {
return new Promise((resolve, reject) => {
const command = `shortcuts view '${params.name}'`;
exec(command, (error, stdout, stderr) => {
if (error) {
reject(
new McpError(
ErrorCode.InternalError,
`Failed to open shortcut: ${error.message}`,
),
);
return;
}
if (stderr) {
reject(
new McpError(
ErrorCode.InternalError,
`Error opening shortcut: ${stderr}`,
),
);
return;
}
resolve({ success: true, message: `Opened shortcut: ${params.name}` });
});
});
};
// Function to execute the run_shortcut tool
const runShortcut = async (params: RunShortcutInput): Promise<ToolResult> => {
return new Promise((resolve, reject) => {
let command = `shortcuts run '${params.name}'`;
const args = ["run", `'${params.name}'`];
const input = params.input || " ";
if (input.includes("/")) {
if (!fs.existsSync(input)) {
throw new McpError(
ErrorCode.InvalidParams,
`Input file does not exist: ${input}`,
);
}
args.push("--input-path");
args.push(`'${input}'`);
} else {
// Create temp file with content
const tmpPath = path.join("/tmp", `shortcut-input-${Date.now()}`);
fs.writeFileSync(tmpPath, input);
args.push("--input-path");
args.push(`'${tmpPath}'`);
}
args.push("|");
args.push("cat");
console.error("Running command: shortcuts", args.join(" "));
exec(`shortcuts ${args.join(" ")}`, (error, stdout, stderr) => {
console.error("Run");
console.error("Error:", error);
console.error("Stdout:", stdout);
console.error("Stderr:", stderr);
if (error) {
reject(
new McpError(
ErrorCode.InternalError,
`Failed to run shortcut: ${error.message}`,
),
);
return;
}
// If there's output, return it
if (stdout.trim()) {
resolve({ success: true, output: stdout.trim() });
} else {
resolve({ success: true, message: `Ran shortcut: ${params.name}` });
}
});
});
};
// Function to sanitize shortcut names for use in command names
export const sanitizeShortcutName = (name: string): string => {
const prefix = "run_shortcut_";
const maxToolNameLength = 64;
const maxSanitizedLength = maxToolNameLength - prefix.length;
let sanitized = name
.toLowerCase()
.replace(/[^a-z0-9_]/g, "_") // Replace non-alphanumeric chars with underscores
.replace(/_+/g, "_") // Replace multiple underscores with a single one
.replace(/^_|_$/g, ""); // Remove leading/trailing underscores
// Truncate if necessary to ensure total tool name length doesn't exceed 64 characters
if (sanitized.length > maxSanitizedLength) {
sanitized = sanitized.substring(0, maxSanitizedLength);
// Remove trailing underscore if truncation resulted in one
sanitized = sanitized.replace(/_$/, "");
}
return sanitized;
};
// Function to fetch all shortcuts and populate the shortcut map
const initializeShortcuts = async (): Promise<void> => {
console.error("Initializing shortcuts...");
try {
await listShortcuts();
} catch (err) {
console.error("Error initializing shortcuts:", err);
}
console.error(`Initialized ${shortcutMap.size} shortcuts`);
};
export const createServer = () => {
const server = new Server(
{
name: "siri-shortcuts-mcp",
version: "0.1.0",
},
{
capabilities: {
tools: {},
resources: {},
prompts: {},
},
},
);
// Initialize the base tools
const getBaseTools = (): Tool[] => {
let runShortcutDescription = "Run a shortcut by name or identifier (UUID) with optional input and output parameters";
// Conditionally inject shortcut list into the description
if (INJECT_SHORTCUT_LIST && shortcutMap.size > 0) {
const shortcutList = Array.from(shortcutMap.keys())
.map(name => {
const identifier = shortcutIdentifierMap.get(name);
if (identifier) {
return `- "${name}" (${identifier})`;
}
return `- "${name}"`;
})
.join('\n');
runShortcutDescription += `\n\nAvailable shortcuts:\n${shortcutList}`;
}
return [
{
name: ToolName.LIST_SHORTCUTS,
description: "List all available Siri shortcuts",
inputSchema: zodToJsonSchema(ListShortcutsSchema) as ToolInput,
run: listShortcuts,
},
{
name: ToolName.OPEN_SHORTCUT,
description: "Open a shortcut in the Shortcuts app",
inputSchema: zodToJsonSchema(OpenShortcutSchema) as ToolInput,
run: (params: any) => openShortcut(params as OpenShortcutInput),
},
{
name: ToolName.RUN_SHORTCUT,
description: runShortcutDescription,
inputSchema: zodToJsonSchema(RunShortcutSchema) as ToolInput,
run: (params: any) => runShortcut(params as RunShortcutInput),
},
];
};
// Generate dynamic tools for each shortcut
const getDynamicShortcutTools = (): Tool[] => {
const dynamicTools: Tool[] = [];
shortcutMap.forEach((sanitizedName, shortcutName) => {
const toolName = `run_shortcut_${sanitizedName}`;
dynamicTools.push({
name: toolName,
description: `Run the "${shortcutName}" shortcut`,
inputSchema: {
type: "object",
properties: {
input: {
type: "string",
description:
"The input to pass to the shortcut. Can be text, or a filepath",
},
},
} as ToolInput,
run: (params: any) =>
runShortcut({ name: shortcutName, input: params.input }),
});
});
return dynamicTools;
};
server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools: Tool[] = [...getBaseTools()];
// Conditionally add dynamic shortcut tools
if (GENERATE_SHORTCUT_TOOLS) {
tools.push(...getDynamicShortcutTools());
}
return { tools };
});
// Handle resources/list requests (even though we don't have any resources)
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return { resources: [] };
});
// Handle prompts/list requests (even though we don't have any prompts)
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return { prompts: [] };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args = {} } = request.params;
// Check if it's a base tool
const isBaseTool = [
ToolName.LIST_SHORTCUTS,
ToolName.OPEN_SHORTCUT,
ToolName.RUN_SHORTCUT,
].includes(name as ToolName);
// Check if it's a dynamic shortcut tool
const isDynamicTool =
GENERATE_SHORTCUT_TOOLS &&
typeof name === "string" && name.startsWith("run_shortcut_");
// If it's neither a base tool nor a dynamic tool, throw an error
if (!isBaseTool && !isDynamicTool) {
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
try {
let result: ToolResult | undefined;
// Execute the appropriate tool based on the name
switch (name as ToolName) {
case ToolName.LIST_SHORTCUTS:
result = await listShortcuts();
break;
case ToolName.OPEN_SHORTCUT:
result = await openShortcut(args as OpenShortcutInput);
break;
case ToolName.RUN_SHORTCUT:
result = await runShortcut(args as RunShortcutInput);
break;
default:
// Handle dynamic shortcut tools
if (isDynamicTool) {
// Extract the shortcut name from the map based on the sanitized name
const sanitizedName = name.replace("run_shortcut_", "");
const shortcutName = Array.from(shortcutMap.entries()).find(
([_, value]) => value === sanitizedName,
)?.[0];
if (!shortcutName) {
throw new McpError(
ErrorCode.InvalidParams,
`No shortcut found for sanitized name: ${sanitizedName}`,
);
}
// Safely extract input from args
const input =
args && typeof args === "object" && "input" in args
? String(args.input)
: undefined;
result = await runShortcut({ name: shortcutName, input });
} else {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`,
);
}
}
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
// Re-throw any errors that occur during execution
throw error instanceof McpError
? error
: new McpError(
ErrorCode.InternalError,
error instanceof Error ? error.message : String(error),
);
}
});
// Initialize shortcuts when the server starts
initializeShortcuts();
return { server, cleanup: async () => {} };
};
```
--------------------------------------------------------------------------------
/shortcuts.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { sanitizeShortcutName, generateUniqueSanitizedName } from './shortcuts.js';
import { z } from 'zod';
// Define the schema to match what's in shortcuts.ts
const RunShortcutSchema = z
.object({
name: z.string().describe("The name or identifier (UUID) of the shortcut to run"),
input: z
.string()
.optional()
.describe(
"The input to pass to the shortcut. Can be text, or a filepath",
),
})
.strict();
describe('sanitizeShortcutName', () => {
it('should convert to lowercase', () => {
expect(sanitizeShortcutName('My Shortcut')).toBe('my_shortcut');
});
it('should replace spaces with underscores', () => {
expect(sanitizeShortcutName('hello world')).toBe('hello_world');
});
it('should replace special characters with underscores', () => {
expect(sanitizeShortcutName('hello@world#test')).toBe('hello_world_test');
});
it('should replace multiple underscores with single underscore', () => {
expect(sanitizeShortcutName('hello___world')).toBe('hello_world');
});
it('should remove leading and trailing underscores', () => {
expect(sanitizeShortcutName('_hello_world_')).toBe('hello_world');
});
it('should preserve alphanumeric characters and underscores', () => {
expect(sanitizeShortcutName('test_123_abc')).toBe('test_123_abc');
});
it('should handle empty string', () => {
expect(sanitizeShortcutName('')).toBe('');
});
it('should handle strings with only special characters', () => {
expect(sanitizeShortcutName('@#$%')).toBe('');
});
it('should truncate long names to fit within 64-character limit', () => {
// "run_shortcut_" is 13 characters, so max sanitized length is 51
const longName = 'a'.repeat(60); // 60 'a' characters
const result = sanitizeShortcutName(longName);
// Should be truncated to 51 characters
expect(result).toBe('a'.repeat(51));
expect(result.length).toBe(51);
// Verify the full tool name would be exactly 64 characters
const fullToolName = `run_shortcut_${result}`;
expect(fullToolName.length).toBe(64);
});
it('should handle truncation and remove trailing underscore if present', () => {
// Create a name that when truncated would end with underscore
const nameWithUnderscores = 'a'.repeat(50) + '_test';
const result = sanitizeShortcutName(nameWithUnderscores);
// Should truncate and remove trailing underscore
expect(result).toBe('a'.repeat(50)); // 50 characters, no trailing underscore
expect(result.length).toBe(50);
});
it('should handle names that are exactly at the limit', () => {
const exactLimitName = 'a'.repeat(51); // Exactly 51 characters
const result = sanitizeShortcutName(exactLimitName);
expect(result).toBe(exactLimitName);
expect(result.length).toBe(51);
});
});
describe('generateUniqueSanitizedName', () => {
it('should return the sanitized name when no conflicts exist', () => {
const existingNames = new Set<string>();
const result = generateUniqueSanitizedName('My Shortcut', existingNames);
expect(result).toBe('my_shortcut');
});
it('should append _1 when the sanitized name conflicts', () => {
const existingNames = new Set(['my_shortcut']);
const result = generateUniqueSanitizedName('My Shortcut', existingNames);
expect(result).toBe('my_shortcut_1');
});
it('should increment counter for multiple conflicts', () => {
const existingNames = new Set(['my_shortcut', 'my_shortcut_1', 'my_shortcut_2']);
const result = generateUniqueSanitizedName('My Shortcut', existingNames);
expect(result).toBe('my_shortcut_3');
});
it('should handle long names with conflicts by truncating base name', () => {
// Create a long name that would need truncation
const longName = 'a'.repeat(60);
const baseSanitized = 'a'.repeat(51); // What sanitizeShortcutName would return
const existingNames = new Set([baseSanitized]);
const result = generateUniqueSanitizedName(longName, existingNames);
// Should truncate base to make room for "_1" suffix
const expectedBase = 'a'.repeat(49); // 49 + 2 ("_1") = 51 total
expect(result).toBe(expectedBase + '_1');
expect(result.length).toBe(51);
// Verify full tool name is within limit
const fullToolName = `run_shortcut_${result}`;
expect(fullToolName.length).toBe(64);
});
it('should handle multiple conflicts with long names', () => {
const longName = 'a'.repeat(60);
const baseSanitized = 'a'.repeat(51);
// Create conflicts up to _9
const existingNames = new Set([
baseSanitized,
'a'.repeat(49) + '_1',
'a'.repeat(49) + '_2',
'a'.repeat(49) + '_3',
'a'.repeat(49) + '_4',
'a'.repeat(49) + '_5',
'a'.repeat(49) + '_6',
'a'.repeat(49) + '_7',
'a'.repeat(49) + '_8',
'a'.repeat(49) + '_9'
]);
const result = generateUniqueSanitizedName(longName, existingNames);
// Should use _10, which requires truncating base further
const expectedBase = 'a'.repeat(48); // 48 + 3 ("_10") = 51 total
expect(result).toBe(expectedBase + '_10');
expect(result.length).toBe(51);
});
it('should handle empty existing names set', () => {
const existingNames = new Set<string>();
const result = generateUniqueSanitizedName('test', existingNames);
expect(result).toBe('test');
});
it('should work with names that sanitize to empty string', () => {
const existingNames = new Set<string>();
const result = generateUniqueSanitizedName('@#$%', existingNames);
// Should return empty string since sanitizeShortcutName returns empty string
expect(result).toBe('');
});
it('should handle conflicts with names that sanitize to empty string', () => {
const existingNames = new Set(['']);
const result = generateUniqueSanitizedName('@#$%', existingNames);
// Should append _1 to empty string
expect(result).toBe('_1');
});
it('should ensure final tool name never exceeds 64 characters', () => {
// Test with various long names and conflict scenarios
const testCases = [
'a'.repeat(100),
'Very Long Shortcut Name That Should Be Truncated',
'Multiple Special Characters !!! @@@ ###',
];
testCases.forEach((testName) => {
const existingNames = new Set<string>();
// Generate 10 variations to test conflict resolution
for (let i = 0; i < 10; i++) {
const result = generateUniqueSanitizedName(testName, existingNames);
existingNames.add(result);
const fullToolName = `run_shortcut_${result}`;
expect(fullToolName.length).toBeLessThanOrEqual(64);
}
});
});
});
describe('Shortcut Identifier Parsing', () => {
it('should parse shortcut names with identifiers correctly', () => {
const testCases = [
{
input: 'Simple Shortcut (D9DBF774-F5CF-4E2E-9418-392951F0C770)',
expected: { name: 'Simple Shortcut', identifier: 'D9DBF774-F5CF-4E2E-9418-392951F0C770' }
},
{
input: 'Shortcut (Name) (D9DBF774-F5CF-4E2E-9418-392951F0C770)',
expected: { name: 'Shortcut (Name)', identifier: 'D9DBF774-F5CF-4E2E-9418-392951F0C770' }
},
{
input: 'Complex (Name) With (Brackets) (D9DBF774-F5CF-4E2E-9418-392951F0C770)',
expected: { name: 'Complex (Name) With (Brackets)', identifier: 'D9DBF774-F5CF-4E2E-9418-392951F0C770' }
},
{
input: 'Shortcut Without UUID',
expected: { name: 'Shortcut Without UUID', identifier: undefined }
},
{
input: '(Leading Bracket) Shortcut (D9DBF774-F5CF-4E2E-9418-392951F0C770)',
expected: { name: '(Leading Bracket) Shortcut', identifier: 'D9DBF774-F5CF-4E2E-9418-392951F0C770' }
}
];
testCases.forEach(testCase => {
const match = testCase.input.match(/^(.+?)\s*\(([A-F0-9-]+)\)$/);
if (match) {
expect(match[1].trim()).toBe(testCase.expected.name);
expect(match[2]).toBe(testCase.expected.identifier);
} else {
expect(testCase.expected.identifier).toBeUndefined();
}
});
});
});
describe('Configuration Environment Variables', () => {
const originalEnv = process.env;
beforeEach(() => {
// Reset modules to allow re-importing with different env vars
vi.resetModules();
});
afterEach(() => {
// Restore original environment
process.env = { ...originalEnv };
vi.restoreAllMocks();
});
describe('GENERATE_SHORTCUT_TOOLS environment variable', () => {
it('should default to true when not set', () => {
delete process.env.GENERATE_SHORTCUT_TOOLS;
// Mock child_process before importing
vi.doMock('child_process', () => ({
exec: vi.fn(),
spawn: vi.fn()
}));
// Since the env vars are read at module import, we need to check the behavior
// by testing if the default value would be true
const shouldGenerate = process.env.GENERATE_SHORTCUT_TOOLS !== "false";
expect(shouldGenerate).toBe(true);
});
it('should be false when explicitly set to "false"', () => {
process.env.GENERATE_SHORTCUT_TOOLS = 'false';
const shouldGenerate = process.env.GENERATE_SHORTCUT_TOOLS !== "false";
expect(shouldGenerate).toBe(false);
});
it('should be true when set to "true"', () => {
process.env.GENERATE_SHORTCUT_TOOLS = 'true';
const shouldGenerate = process.env.GENERATE_SHORTCUT_TOOLS !== "false";
expect(shouldGenerate).toBe(true);
});
it('should be true when set to any other value', () => {
process.env.GENERATE_SHORTCUT_TOOLS = 'yes';
const shouldGenerate = process.env.GENERATE_SHORTCUT_TOOLS !== "false";
expect(shouldGenerate).toBe(true);
});
});
describe('INJECT_SHORTCUT_LIST environment variable', () => {
it('should default to false when not set', () => {
delete process.env.INJECT_SHORTCUT_LIST;
const shouldInject = process.env.INJECT_SHORTCUT_LIST === "true";
expect(shouldInject).toBe(false);
});
it('should be true when set to "true"', () => {
process.env.INJECT_SHORTCUT_LIST = 'true';
const shouldInject = process.env.INJECT_SHORTCUT_LIST === "true";
expect(shouldInject).toBe(true);
});
it('should be false when set to "false"', () => {
process.env.INJECT_SHORTCUT_LIST = 'false';
const shouldInject = process.env.INJECT_SHORTCUT_LIST === "true";
expect(shouldInject).toBe(false);
});
it('should be false when set to any other value', () => {
process.env.INJECT_SHORTCUT_LIST = 'yes';
const shouldInject = process.env.INJECT_SHORTCUT_LIST === "true";
expect(shouldInject).toBe(false);
});
});
});
describe('Shortcut Identifier Support', () => {
it('should document that run_shortcut accepts UUID identifiers', () => {
// This test verifies that the schema correctly documents UUID support
const schema = RunShortcutSchema.shape;
expect(schema.name._def.description).toContain('identifier (UUID)');
});
it('should validate UUID format in schema', () => {
// Test that the schema accepts valid UUIDs
const validInput = {
name: 'D9DBF774-F5CF-4E2E-9418-392951F0C770',
input: 'test input'
};
const result = RunShortcutSchema.safeParse(validInput);
expect(result.success).toBe(true);
});
it('should validate shortcut names with identifiers', () => {
// Test that the schema accepts names with identifiers
const validInput = {
name: 'Video To GIF 1 (D9DBF774-F5CF-4E2E-9418-392951F0C770)',
input: 'test input'
};
const result = RunShortcutSchema.safeParse(validInput);
expect(result.success).toBe(true);
});
});
describe('Server Configuration Integration', () => {
const originalEnv = process.env;
beforeEach(() => {
vi.resetModules();
vi.restoreAllMocks();
});
afterEach(() => {
process.env = { ...originalEnv };
vi.restoreAllMocks();
});
it('should read environment variables correctly at module level', async () => {
// Set environment variables before importing
process.env.GENERATE_SHORTCUT_TOOLS = 'false';
process.env.INJECT_SHORTCUT_LIST = 'true';
// Mock child_process
const mockExec = vi.fn((command, callback) => {
if (command === 'shortcuts list --show-identifiers') {
callback(null, 'Test Shortcut 1 (UUID-1234)\nTest Shortcut 2 (UUID-5678)\n', '');
}
});
vi.doMock('child_process', () => ({
exec: mockExec,
spawn: vi.fn()
}));
// Import after setting env vars and mocks
const shortcuts = await import('./shortcuts.js');
// Test that the module imported successfully with the env vars
expect(shortcuts).toBeDefined();
expect(shortcuts.createServer).toBeDefined();
});
it('should handle shortcut list injection in description', () => {
// Test the logic for injecting shortcut list
const shortcutMap = new Map([
['Test Shortcut 1', 'test_shortcut_1'],
['Test Shortcut 2', 'test_shortcut_2']
]);
const shortcutIdentifierMap = new Map([
['Test Shortcut 1', 'UUID-1234'],
['Test Shortcut 2', 'UUID-5678']
]);
const INJECT_SHORTCUT_LIST = true;
let runShortcutDescription = "Run a shortcut by name or identifier (UUID) with optional input and output parameters";
// Simulate the description injection logic from the code
if (INJECT_SHORTCUT_LIST && shortcutMap.size > 0) {
const shortcutList = Array.from(shortcutMap.keys())
.map(name => {
const identifier = shortcutIdentifierMap.get(name);
if (identifier) {
return `- "${name}" (${identifier})`;
}
return `- "${name}"`;
})
.join('\n');
runShortcutDescription += `\n\nAvailable shortcuts:\n${shortcutList}`;
}
expect(runShortcutDescription).toContain('Available shortcuts:');
expect(runShortcutDescription).toContain('- "Test Shortcut 1" (UUID-1234)');
expect(runShortcutDescription).toContain('- "Test Shortcut 2" (UUID-5678)');
});
it('should not inject shortcut list when INJECT_SHORTCUT_LIST is false', () => {
const shortcutMap = new Map([
['Test Shortcut 1', 'test_shortcut_1'],
['Test Shortcut 2', 'test_shortcut_2']
]);
const shortcutIdentifierMap = new Map([
['Test Shortcut 1', 'UUID-1234'],
['Test Shortcut 2', 'UUID-5678']
]);
const INJECT_SHORTCUT_LIST = false;
let runShortcutDescription = "Run a shortcut by name or identifier (UUID) with optional input and output parameters";
// Simulate the description injection logic from the code
if (INJECT_SHORTCUT_LIST && shortcutMap.size > 0) {
const shortcutList = Array.from(shortcutMap.keys())
.map(name => {
const identifier = shortcutIdentifierMap.get(name);
if (identifier) {
return `- "${name}" (${identifier})`;
}
return `- "${name}"`;
})
.join('\n');
runShortcutDescription += `\n\nAvailable shortcuts:\n${shortcutList}`;
}
expect(runShortcutDescription).not.toContain('Available shortcuts:');
expect(runShortcutDescription).not.toContain('Test Shortcut 1');
});
it('should not inject shortcut list when no shortcuts are available', () => {
const shortcutMap = new Map<string, string>();
const shortcutIdentifierMap = new Map<string, string>();
const INJECT_SHORTCUT_LIST = true;
let runShortcutDescription = "Run a shortcut by name or identifier (UUID) with optional input and output parameters";
// Simulate the description injection logic from the code
if (INJECT_SHORTCUT_LIST && shortcutMap.size > 0) {
const shortcutList = Array.from(shortcutMap.keys())
.map(name => {
const identifier = shortcutIdentifierMap.get(name);
if (identifier) {
return `- "${name}" (${identifier})`;
}
return `- "${name}"`;
})
.join('\n');
runShortcutDescription += `\n\nAvailable shortcuts:\n${shortcutList}`;
}
expect(runShortcutDescription).not.toContain('Available shortcuts:');
});
it('should validate tool names based on GENERATE_SHORTCUT_TOOLS setting', () => {
// Test the logic for validating dynamic tool calls
const GENERATE_SHORTCUT_TOOLS = false;
const toolName = 'run_shortcut_test_shortcut_1';
// Base tools
const baseTool = ['list_shortcuts', 'open_shortcut', 'run_shortcut'].includes(toolName);
// Dynamic tool check
const isDynamicTool = GENERATE_SHORTCUT_TOOLS && toolName.startsWith('run_shortcut_');
const isValidTool = baseTool || isDynamicTool;
expect(baseTool).toBe(false); // This is not a base tool
expect(isDynamicTool).toBe(false); // Dynamic tools are disabled
expect(isValidTool).toBe(false); // So this tool should not be valid
});
it('should allow dynamic tools when GENERATE_SHORTCUT_TOOLS is true', () => {
const GENERATE_SHORTCUT_TOOLS = true;
const toolName = 'run_shortcut_test_shortcut_1';
// Base tools
const baseTool = ['list_shortcuts', 'open_shortcut', 'run_shortcut'].includes(toolName);
// Dynamic tool check
const isDynamicTool = GENERATE_SHORTCUT_TOOLS && toolName.startsWith('run_shortcut_');
const isValidTool = baseTool || isDynamicTool;
expect(baseTool).toBe(false); // This is not a base tool
expect(isDynamicTool).toBe(true); // Dynamic tools are enabled and this matches the pattern
expect(isValidTool).toBe(true); // So this tool should be valid
});
it('should always allow base tools regardless of GENERATE_SHORTCUT_TOOLS setting', () => {
const baseTools = ['list_shortcuts', 'open_shortcut', 'run_shortcut'];
// Test with GENERATE_SHORTCUT_TOOLS = false
let GENERATE_SHORTCUT_TOOLS = false;
baseTools.forEach(toolName => {
const baseTool = baseTools.includes(toolName);
const isDynamicTool = GENERATE_SHORTCUT_TOOLS && toolName.startsWith('run_shortcut_');
const isValidTool = baseTool || isDynamicTool;
expect(isValidTool).toBe(true);
});
// Test with GENERATE_SHORTCUT_TOOLS = true
GENERATE_SHORTCUT_TOOLS = true;
baseTools.forEach(toolName => {
const baseTool = baseTools.includes(toolName);
const isDynamicTool = GENERATE_SHORTCUT_TOOLS && toolName.startsWith('run_shortcut_');
const isValidTool = baseTool || isDynamicTool;
expect(isValidTool).toBe(true);
});
});
});
```