Skip to main content
Glama
shortcutClient.ts5.08 kB
import type { ShortcutConfig, Story, StoryComment, CreateStoryComment, CreateStoryParams, UpdateStory, Workflow, Project, Epic, Member, SearchResults, UploadedFile, } from './shortcut-types' import { isTextFile } from './mimeTypes' export class ShortcutClient { private baseUrl: string private headers: Record<string, string> constructor(config: ShortcutConfig) { this.baseUrl = config.baseUrl this.headers = { 'Content-Type': 'application/json', 'Shortcut-Token': config.apiToken, } } private async request<T>(path: string, options: RequestInit = {}): Promise<T> { const url = `${this.baseUrl}${path}` const response = await fetch(url, { ...options, headers: { ...this.headers, ...options.headers, }, }) if (!response.ok) { const errorData = await response.json().catch(() => ({})) const error = new Error(`Shortcut API error: ${response.statusText}`) ;(error as unknown as { response: { status: number; data: unknown } }).response = { status: response.status, data: errorData, } throw error } return response.json() as Promise<T> } async getStory(storyId: number): Promise<Story> { return this.request<Story>(`/stories/${storyId}`) } async createStory(params: CreateStoryParams): Promise<Story> { return this.request<Story>('/stories', { method: 'POST', body: JSON.stringify(params), }) } async searchStories(query: string): Promise<SearchResults> { const params = new URLSearchParams({ query, page_size: '25' }) return this.request<SearchResults>(`/search?${params.toString()}`) } async getStoriesInProject(projectId: number): Promise<Story[]> { const params = new URLSearchParams({ query: `project:${projectId}`, page_size: '100', }) const result = await this.request<{ data: Story[] }>(`/search/stories?${params.toString()}`) return result.data } async getStoriesInEpic(epicId: number): Promise<Story[]> { const params = new URLSearchParams({ query: `epic:${epicId}`, page_size: '100', }) const result = await this.request<{ data: Story[] }>(`/search/stories?${params.toString()}`) return result.data } async updateStory(storyId: number, update: UpdateStory): Promise<Story> { return this.request<Story>(`/stories/${storyId}`, { method: 'PUT', body: JSON.stringify(update), }) } async addComment(storyId: number, comment: CreateStoryComment): Promise<StoryComment> { return this.request<StoryComment>(`/stories/${storyId}/comments`, { method: 'POST', body: JSON.stringify(comment), }) } async getComments(storyId: number): Promise<StoryComment[]> { return this.request<StoryComment[]>(`/stories/${storyId}/comments`) } async getWorkflows(): Promise<Workflow[]> { return this.request<Workflow[]>('/workflows') } async getProjects(): Promise<Project[]> { return this.request<Project[]>('/projects') } async getEpics(): Promise<Epic[]> { return this.request<Epic[]>('/epics') } async getMembers(): Promise<Member[]> { return this.request<Member[]>('/members') } async getCurrentUser(): Promise<Member> { return this.request<Member>('/member') } async getFile(fileId: number): Promise<UploadedFile> { return this.request<UploadedFile>(`/files/${fileId}`) } async downloadFile(fileUrl: string): Promise<{ content: Buffer; contentType: string }> { // File URLs from Shortcut require authentication const response = await fetch(fileUrl, { headers: { 'Shortcut-Token': this.headers['Shortcut-Token'], }, }) if (!response.ok) { throw new Error(`Failed to download file: ${response.statusText}`) } const buffer = Buffer.from(await response.arrayBuffer()) const contentType = response.headers.get('content-type') || 'application/octet-stream' return { content: buffer, contentType } } async getStoryFiles(storyId: number): Promise<UploadedFile[]> { // Get the full story with files included const story = await this.getStory(storyId) return story.files || [] } async downloadFileAsText(fileUrl: string): Promise<string> { const { content, contentType } = await this.downloadFile(fileUrl) // Use comprehensive mime type detection if (isTextFile(contentType, fileUrl)) { return content.toString('utf-8') } // For binary files, return base64 encoded return content.toString('base64') } }

Latest Blog Posts

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/currentspace/shortcut_mcp'

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