Skip to main content
Glama
iceener

Linear Streamable MCP Server

by iceener
workspace-metadata.test.ts12.7 kB
/** * Tests for workspace_metadata tool. * Verifies: input validation, output shape, viewer/teams/states/labels/projects fetching. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { z } from 'zod'; import { workspaceMetadataTool } from '../../src/shared/tools/linear/workspace-metadata.js'; import { createMockLinearClient, resetMockCalls, type MockLinearClient } from '../mocks/linear-client.js'; import type { ToolContext } from '../../src/shared/tools/types.js'; import workspaceMetadataFixtures from '../fixtures/tool-inputs/workspace-metadata.json'; // ───────────────────────────────────────────────────────────────────────────── // Test Setup // ───────────────────────────────────────────────────────────────────────────── let mockClient: MockLinearClient; const baseContext: ToolContext = { sessionId: 'test-session', providerToken: 'test-token', authStrategy: 'bearer', }; // Mock the getLinearClient function vi.mock('../../src/services/linear/client.js', () => ({ getLinearClient: vi.fn(() => Promise.resolve(mockClient)), })); beforeEach(() => { mockClient = createMockLinearClient(); resetMockCalls(mockClient); }); // ───────────────────────────────────────────────────────────────────────────── // Tool Metadata Tests // ───────────────────────────────────────────────────────────────────────────── describe('workspace_metadata tool metadata', () => { it('has correct name and title', () => { expect(workspaceMetadataTool.name).toBe('workspace_metadata'); expect(workspaceMetadataTool.title).toBe('Discover IDs (Use First)'); }); it('has readOnlyHint annotation', () => { expect(workspaceMetadataTool.annotations?.readOnlyHint).toBe(true); expect(workspaceMetadataTool.annotations?.destructiveHint).toBe(false); }); it('has description for LLM', () => { expect(workspaceMetadataTool.description).toContain('discover'); expect(workspaceMetadataTool.description).toContain('viewer'); expect(workspaceMetadataTool.description).toContain('teams'); }); }); // ───────────────────────────────────────────────────────────────────────────── // Input Validation Tests // ───────────────────────────────────────────────────────────────────────────── describe('workspace_metadata input validation', () => { describe('valid inputs', () => { for (const fixture of workspaceMetadataFixtures.valid) { it(`accepts: ${fixture.name}`, () => { const result = workspaceMetadataTool.inputSchema.safeParse(fixture.input); expect(result.success).toBe(true); }); } }); describe('invalid inputs', () => { for (const fixture of workspaceMetadataFixtures.invalid) { it(`rejects: ${fixture.name}`, () => { const result = workspaceMetadataTool.inputSchema.safeParse(fixture.input); expect(result.success).toBe(false); if (!result.success) { const errorMessage = result.error.errors.map((e) => e.message).join(', '); expect(errorMessage).toContain(fixture.expectedError); } }); } }); }); // ───────────────────────────────────────────────────────────────────────────── // Handler Behavior Tests // ───────────────────────────────────────────────────────────────────────────── describe('workspace_metadata handler', () => { it('returns viewer profile when include contains "profile"', async () => { const result = await workspaceMetadataTool.handler({ include: ['profile'] }, baseContext); expect(result.isError).toBeFalsy(); expect(result.structuredContent).toBeDefined(); const structured = result.structuredContent as Record<string, unknown>; expect(structured.viewer).toBeDefined(); const viewer = structured.viewer as Record<string, unknown>; expect(viewer.id).toBe('user-001'); expect(viewer.name).toBe('Test User'); expect(viewer.email).toBe('test@example.com'); expect(viewer.timezone).toBe('Europe/Warsaw'); }); it('returns teams when include contains "teams"', async () => { const result = await workspaceMetadataTool.handler({ include: ['teams'] }, baseContext); expect(result.isError).toBeFalsy(); const structured = result.structuredContent as Record<string, unknown>; expect(structured.teams).toBeDefined(); expect(Array.isArray(structured.teams)).toBe(true); const teams = structured.teams as Array<Record<string, unknown>>; expect(teams.length).toBe(2); expect(teams[0].id).toBe('team-eng'); expect(teams[0].key).toBe('ENG'); expect(teams[0].name).toBe('Engineering'); expect(teams[0].cyclesEnabled).toBe(true); }); it('returns workflow states by team when include contains "workflow_states"', async () => { const result = await workspaceMetadataTool.handler( { include: ['teams', 'workflow_states'] }, baseContext, ); expect(result.isError).toBeFalsy(); const structured = result.structuredContent as Record<string, unknown>; expect(structured.workflowStatesByTeam).toBeDefined(); const statesByTeam = structured.workflowStatesByTeam as Record<string, unknown[]>; expect(statesByTeam['team-eng']).toBeDefined(); expect(statesByTeam['team-eng'].length).toBe(5); const inProgressState = statesByTeam['team-eng'].find( (s: unknown) => (s as Record<string, unknown>).name === 'In Progress', ); expect(inProgressState).toBeDefined(); expect((inProgressState as Record<string, unknown>).type).toBe('started'); }); it('returns labels by team when include contains "labels"', async () => { const result = await workspaceMetadataTool.handler( { include: ['teams', 'labels'] }, baseContext, ); expect(result.isError).toBeFalsy(); const structured = result.structuredContent as Record<string, unknown>; expect(structured.labelsByTeam).toBeDefined(); const labelsByTeam = structured.labelsByTeam as Record<string, unknown[]>; expect(labelsByTeam['team-eng']).toBeDefined(); expect(labelsByTeam['team-eng'].length).toBe(3); const bugLabel = labelsByTeam['team-eng'].find( (l: unknown) => (l as Record<string, unknown>).name === 'Bug', ); expect(bugLabel).toBeDefined(); expect((bugLabel as Record<string, unknown>).color).toBe('#ff0000'); }); it('returns projects when include contains "projects"', async () => { const result = await workspaceMetadataTool.handler( { include: ['teams', 'projects'] }, baseContext, ); expect(result.isError).toBeFalsy(); const structured = result.structuredContent as Record<string, unknown>; expect(structured.projects).toBeDefined(); const projects = structured.projects as Array<Record<string, unknown>>; expect(projects.length).toBeGreaterThan(0); expect(projects[0].name).toBe('Q1 Release'); expect(projects[0].state).toBe('started'); expect(projects[0].teamId).toBe('team-eng'); }); it('returns default includes when no include specified', async () => { const result = await workspaceMetadataTool.handler({}, baseContext); expect(result.isError).toBeFalsy(); const structured = result.structuredContent as Record<string, unknown>; // Default: profile, teams, workflow_states, labels, projects expect(structured.viewer).toBeDefined(); expect(structured.teams).toBeDefined(); expect(structured.workflowStatesByTeam).toBeDefined(); expect(structured.labelsByTeam).toBeDefined(); expect(structured.projects).toBeDefined(); // favorites is NOT included by default expect(structured.favorites).toBeUndefined(); }); it('filters teams by teamIds when provided', async () => { const result = await workspaceMetadataTool.handler( { include: ['teams'], teamIds: ['team-eng'] }, baseContext, ); expect(result.isError).toBeFalsy(); const structured = result.structuredContent as Record<string, unknown>; const teams = structured.teams as Array<Record<string, unknown>>; expect(teams.length).toBe(1); expect(teams[0].id).toBe('team-eng'); // Verify team() was called with the ID expect(mockClient.team).toHaveBeenCalledWith('team-eng'); }); it('respects project_limit parameter', async () => { const result = await workspaceMetadataTool.handler( { include: ['teams', 'projects'], project_limit: 1 }, baseContext, ); expect(result.isError).toBeFalsy(); const structured = result.structuredContent as Record<string, unknown>; const projects = structured.projects as Array<Record<string, unknown>>; // With 2 teams and limit 1, should get at most 2 projects (1 per team) expect(projects.length).toBeLessThanOrEqual(2); }); it('includes summary with counts', async () => { const result = await workspaceMetadataTool.handler({}, baseContext); expect(result.isError).toBeFalsy(); const structured = result.structuredContent as Record<string, unknown>; expect(structured.summary).toBeDefined(); const summary = structured.summary as Record<string, number>; expect(typeof summary.teamCount).toBe('number'); expect(typeof summary.stateCount).toBe('number'); expect(typeof summary.labelCount).toBe('number'); expect(typeof summary.projectCount).toBe('number'); }); it('returns text content with viewer info', async () => { const result = await workspaceMetadataTool.handler({ include: ['profile'] }, baseContext); expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); const textContent = result.content[0]; expect(textContent.type).toBe('text'); // Text should include viewer identification expect(textContent.text).toContain('Test User'); expect(textContent.text).toContain('user-001'); // Verify structuredContent matches text claims const structured = result.structuredContent as Record<string, unknown>; const viewer = structured.viewer as Record<string, unknown>; expect(viewer.id).toBe('user-001'); expect(viewer.name).toBe('Test User'); }); }); // ───────────────────────────────────────────────────────────────────────────── // Output Schema Tests // ───────────────────────────────────────────────────────────────────────────── describe('workspace_metadata output schema compliance', () => { it('structuredContent matches AccountOutputSchema shape', async () => { const result = await workspaceMetadataTool.handler({}, baseContext); expect(result.structuredContent).toBeDefined(); const structured = result.structuredContent as Record<string, unknown>; // Verify required fields expect(structured.summary).toBeDefined(); // Verify viewer shape if present if (structured.viewer) { const viewer = structured.viewer as Record<string, unknown>; expect(typeof viewer.id).toBe('string'); } // Verify teams shape if present if (structured.teams) { const teams = structured.teams as Array<Record<string, unknown>>; for (const team of teams) { expect(typeof team.id).toBe('string'); expect(typeof team.name).toBe('string'); } } }); });

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/iceener/linear-streamable-mcp-server'

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