Skip to main content
Glama

Shortcut MCP Server

Official
by useshortcut
iterations.ts9.35 kB
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import type { ShortcutClientWrapper } from "@/client/shortcut"; import type { CustomMcpServer } from "@/mcp/CustomMcpServer"; import { BaseTools } from "./base"; import { buildSearchQuery, type QueryParams } from "./utils/search"; import { date } from "./utils/validation"; export class IterationTools extends BaseTools { static create(client: ShortcutClientWrapper, server: CustomMcpServer) { const tools = new IterationTools(client); server.addToolWithReadAccess( "iterations-get-stories", "Get stories in a specific iteration by iteration public ID", { iterationPublicId: z.number().positive().describe("The public ID of the iteration"), includeStoryDescriptions: z .boolean() .optional() .default(false) .describe( "Indicate whether story descriptions should be included. Including descriptions may take longer and will increase the size of the response.", ), }, async ({ iterationPublicId, includeStoryDescriptions }) => await tools.getIterationStories(iterationPublicId, includeStoryDescriptions), ); server.addToolWithReadAccess( "iterations-get-by-id", "Get a Shortcut iteration by public ID", { iterationPublicId: z.number().positive().describe("The public ID of the iteration to get"), full: z .boolean() .optional() .default(false) .describe( "True to return all iteration fields from the API. False to return a slim version that excludes uncommon fields", ), }, async ({ iterationPublicId, full }) => await tools.getIteration(iterationPublicId, full), ); server.addToolWithReadAccess( "iterations-search", "Find Shortcut iterations.", { nextPageToken: z .string() .optional() .describe( "If a next_page_token was returned from the search result, pass it in to get the next page of results. Should be combined with the original search parameters.", ), id: z.number().optional().describe("Find only iterations with the specified public ID"), name: z.string().optional().describe("Find only iterations matching the specified name"), description: z .string() .optional() .describe("Find only iterations matching the specified description"), state: z .enum(["started", "unstarted", "done"]) .optional() .describe("Find only iterations matching the specified state"), team: z .string() .optional() .describe( "Find only iterations matching the specified team. This can be a team ID or mention name.", ), created: date(), updated: date(), startDate: date(), endDate: date(), }, async ({ nextPageToken, ...params }) => await tools.searchIterations(params, nextPageToken), ); server.addToolWithWriteAccess( "iterations-create", "Create a new Shortcut iteration", { name: z.string().describe("The name of the iteration"), startDate: z.string().describe("The start date of the iteration in YYYY-MM-DD format"), endDate: z.string().describe("The end date of the iteration in YYYY-MM-DD format"), teamId: z.string().optional().describe("The ID of a team to assign the iteration to"), description: z.string().optional().describe("A description of the iteration"), }, async (params) => await tools.createIteration(params), ); server.addToolWithReadAccess( "iterations-get-active", "Get the active Shortcut iterations for the current user based on their team memberships", { teamId: z.string().optional().describe("The ID of a team to filter iterations by"), }, async ({ teamId }) => await tools.getActiveIterations(teamId), ); server.addToolWithReadAccess( "iterations-get-upcoming", "Get the upcoming Shortcut iterations for the current user based on their team memberships", { teamId: z.string().optional().describe("The ID of a team to filter iterations by"), }, async ({ teamId }) => await tools.getUpcomingIterations(teamId), ); return tools; } async getIterationStories(iterationPublicId: number, includeDescription: boolean) { const { stories } = await this.client.listIterationStories( iterationPublicId, includeDescription, ); if (!stories) throw new Error( `Failed to retrieve Shortcut stories in iteration with public ID: ${iterationPublicId}.`, ); return this.toResult( `Result (${stories.length} stories found):`, await this.entitiesWithRelatedEntities(stories, "stories"), ); } async searchIterations(params: QueryParams, nextToken?: string) { const currentUser = await this.client.getCurrentUser(); const query = await buildSearchQuery(params, currentUser); const { iterations, total, next_page_token } = await this.client.searchIterations( query, nextToken, ); if (!iterations) throw new Error(`Failed to search for iterations matching your query: "${query}".`); if (!iterations.length) return this.toResult(`Result: No iterations found.`); return this.toResult( `Result (${iterations.length} shown of ${total} total iterations found):`, await this.entitiesWithRelatedEntities(iterations, "iterations"), next_page_token, ); } async getIteration(iterationPublicId: number, full = false) { const iteration = await this.client.getIteration(iterationPublicId); if (!iteration) throw new Error( `Failed to retrieve Shortcut iteration with public ID: ${iterationPublicId}.`, ); return this.toResult( `Iteration: ${iterationPublicId}`, await this.entityWithRelatedEntities(iteration, "iteration", full), ); } async createIteration({ name, startDate, endDate, teamId, description, }: { name: string; startDate: string; endDate: string; teamId?: string; description?: string; }): Promise<CallToolResult> { const iteration = await this.client.createIteration({ name, start_date: startDate, end_date: endDate, group_ids: teamId ? [teamId] : undefined, description, }); if (!iteration) throw new Error(`Failed to create the iteration.`); return this.toResult(`Iteration created with ID: ${iteration.id}.`); } async getActiveIterations(teamId?: string) { if (teamId) { const team = await this.client.getTeam(teamId); if (!team) throw new Error(`No team found matching id: "${teamId}"`); const result = await this.client.getActiveIteration([teamId]); const iterations = result.get(teamId); if (!iterations?.length) return this.toResult(`Result: No active iterations found for team.`); if (iterations.length === 1) return this.toResult( "The active iteration for the team is:", await this.entityWithRelatedEntities(iterations[0], "iteration"), ); return this.toResult( "The active iterations for the team are:", await this.entitiesWithRelatedEntities(iterations, "iterations"), ); } const currentUser = await this.client.getCurrentUser(); if (!currentUser) throw new Error("Failed to retrieve current user."); const teams = await this.client.getTeams(); const teamIds = teams .filter((team) => team.member_ids.includes(currentUser.id)) .map((team) => team.id); if (!teamIds.length) throw new Error("Current user does not belong to any teams."); const resultsByTeam = await this.client.getActiveIteration(teamIds); const allActiveIterations = [...resultsByTeam.values()].flat(); if (!allActiveIterations.length) return this.toResult("Result: No active iterations found for any of your teams."); return this.toResult( `You have ${allActiveIterations.length} active iterations for your teams:`, await this.entitiesWithRelatedEntities(allActiveIterations, "iterations"), ); } async getUpcomingIterations(teamId?: string) { if (teamId) { const team = await this.client.getTeam(teamId); if (!team) throw new Error(`No team found matching id: "${teamId}"`); const result = await this.client.getUpcomingIteration([teamId]); const iterations = result.get(teamId); if (!iterations?.length) return this.toResult(`Result: No upcoming iterations found for team.`); if (iterations.length === 1) return this.toResult( "The next upcoming iteration for the team is:", await this.entityWithRelatedEntities(iterations[0], "iteration"), ); return this.toResult( "The next upcoming iterations for the team are:", await this.entitiesWithRelatedEntities(iterations, "iterations"), ); } const currentUser = await this.client.getCurrentUser(); if (!currentUser) throw new Error("Failed to retrieve current user."); const teams = await this.client.getTeams(); const teamIds = teams .filter((team) => team.member_ids.includes(currentUser.id)) .map((team) => team.id); if (!teamIds.length) throw new Error("Current user does not belong to any teams."); const resultsByTeam = await this.client.getUpcomingIteration(teamIds); const allUpcomingIterations = [...resultsByTeam.values()].flat(); if (!allUpcomingIterations.length) return this.toResult("Result: No upcoming iterations found for any of your teams."); return this.toResult( "The upcoming iterations for all your teams are:", await this.entitiesWithRelatedEntities(allUpcomingIterations, "iterations"), ); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/useshortcut/mcp-server-shortcut'

If you have feedback or need assistance with the MCP directory API, please join our Discord server