Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion packages/local-mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
* for AI models to interact with Glean's capabilities. It uses stdio
* for communication and implements the MCP specification for tool discovery and execution.
*
* The server exposes four tools:
* The server exposes five tools:
* 1. company_search - Search across Glean's indexed content
* 2. people_profile_search - Search for people profiles inside the company
* 3. chat - Converse with Glean's AI assistant
* 4. read_documents - Retrieve documents by ID or URL
* 5. escalations_get - List and search escalations with pagination
*/

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
Expand All @@ -24,6 +25,7 @@ import * as search from './tools/search.js';
import * as chat from './tools/chat.js';
import * as peopleProfileSearch from './tools/people_profile_search.js';
import * as readDocuments from './tools/read_documents.js';
import * as escalations from './tools/escalations.js';
import {
formatGleanError,
isGleanError,
Expand All @@ -35,6 +37,7 @@ export const TOOL_NAMES = {
peopleProfileSearch: 'people_profile_search',
chat: 'chat',
readDocuments: 'read_documents',
escalationsGet: 'escalations_get',
};

/**
Expand Down Expand Up @@ -121,6 +124,20 @@ export async function listToolsHandler() {
`,
inputSchema: z.toJSONSchema(readDocuments.ToolReadDocumentsSchema),
},
{
name: TOOL_NAMES.escalationsGet,
description: `List and search escalations with pagination

Example request:

{
"query": "auth service outage",
"limit": 10,
"offset": 0
}
`,
inputSchema: z.toJSONSchema(escalations.ToolEscalationsGetSchema),
},
],
};
}
Expand Down Expand Up @@ -183,6 +200,19 @@ export async function callToolHandler(request: CallToolRequest) {
};
}

case TOOL_NAMES.escalationsGet: {
const args = escalations.ToolEscalationsGetSchema.parse(
request.params.arguments,
);
const result = await escalations.escalationsGet(args);
const formattedResults = escalations.formatResponse(result);

return {
content: [{ type: 'text', text: formattedResults }],
isError: false,
};
}

default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
Expand Down
51 changes: 51 additions & 0 deletions packages/local-mcp-server/src/test/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('MCP Server Handlers (integration)', () => {
TOOL_NAMES.companySearch,
TOOL_NAMES.chat,
TOOL_NAMES.peopleProfileSearch,
TOOL_NAMES.escalationsGet,
]),
);

Expand Down Expand Up @@ -213,6 +214,56 @@ describe('MCP Server Handlers (integration)', () => {
`);
});

it('executes escalations_get tool with default pagination', async () => {
const req = CallToolRequestSchema.parse({
method: 'tools/call',
id: '9',
jsonrpc: '2.0',
params: {
name: TOOL_NAMES.escalationsGet,
arguments: {},
},
});

const res = await callToolHandler(req);
expect(res.isError).toBe(false);
expect(res.content[0].text).toContain('3 escalations');
expect(res.content[0].text).toContain('Auth service outage');
});

it('executes escalations_get tool with query filter', async () => {
const req = CallToolRequestSchema.parse({
method: 'tools/call',
id: '10',
jsonrpc: '2.0',
params: {
name: TOOL_NAMES.escalationsGet,
arguments: { query: 'auth', limit: 10 },
},
});

const res = await callToolHandler(req);
expect(res.isError).toBe(false);
expect(res.content[0].text).toContain('1 escalation');
expect(res.content[0].text).toContain('Auth service outage');
});

it('executes escalations_get tool with pagination', async () => {
const req = CallToolRequestSchema.parse({
method: 'tools/call',
id: '11',
jsonrpc: '2.0',
params: {
name: TOOL_NAMES.escalationsGet,
arguments: { limit: 1, offset: 0 },
},
});

const res = await callToolHandler(req);
expect(res.isError).toBe(false);
expect(res.content[0].text).toContain('more results available');
});

it('validation error when pageSize is out of range', async () => {
const badReq = CallToolRequestSchema.parse({
method: 'tools/call',
Expand Down
196 changes: 196 additions & 0 deletions packages/local-mcp-server/src/test/tools/escalations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
ToolEscalationsGetSchema,
escalationsGet,
formatResponse,
} from '../../tools/escalations.js';
import { z } from 'zod';
import '@gleanwork/mcp-test-utils/mocks/setup';

describe('Escalations Get Tool', () => {
beforeEach(() => {
delete process.env.GLEAN_URL;
process.env.GLEAN_INSTANCE = 'test';
process.env.GLEAN_API_TOKEN = 'test-token';
});

afterEach(() => {
delete process.env.GLEAN_INSTANCE;
delete process.env.GLEAN_API_TOKEN;
});

describe('JSON Schema Generation', () => {
it('generates correct JSON schema', () => {
expect(z.toJSONSchema(ToolEscalationsGetSchema)).toMatchInlineSnapshot(`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"limit": {
"description": "Maximum number of escalations to return (1-100, default 50).",
"maximum": 100,
"minimum": 1,
"type": "integer",
},
"offset": {
"description": "Number of results to skip for pagination (default 0).",
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer",
},
"query": {
"description": "Optional search query to filter escalations.",
"type": "string",
},
},
"type": "object",
}
`);
});
});

describe('Schema Validation', () => {
it('accepts empty params with defaults', () => {
const result = ToolEscalationsGetSchema.safeParse({});
expect(result.success).toBe(true);
});

it('accepts a query', () => {
const result = ToolEscalationsGetSchema.safeParse({
query: 'auth outage',
});
expect(result.success).toBe(true);
});

it('accepts limit and offset', () => {
const result = ToolEscalationsGetSchema.safeParse({
limit: 10,
offset: 20,
});
expect(result.success).toBe(true);
});

it('rejects limit above max', () => {
const result = ToolEscalationsGetSchema.safeParse({ limit: 200 });
expect(result.success).toBe(false);
});

it('rejects negative offset', () => {
const result = ToolEscalationsGetSchema.safeParse({ offset: -1 });
expect(result.success).toBe(false);
});

it('rejects non-integer limit', () => {
const result = ToolEscalationsGetSchema.safeParse({ limit: 10.5 });
expect(result.success).toBe(false);
});
});

describe('Tool Implementation', () => {
it('returns escalations with default pagination', async () => {
const response = (await escalationsGet({})) as any;

expect(response.results).toBeInstanceOf(Array);
expect(response.results.length).toBe(3);
expect(response.totalCount).toBe(3);
expect(response.hasMoreResults).toBe(false);
});

it('filters by query', async () => {
const response = (await escalationsGet({ query: 'auth' })) as any;

expect(response.results).toBeInstanceOf(Array);
expect(response.results.length).toBe(1);
expect(response.results[0].title).toContain('Auth');
});

it('paginates with limit and offset', async () => {
const response = (await escalationsGet({
limit: 1,
offset: 1,
})) as any;

expect(response.results.length).toBe(1);
expect(response.results[0].id).toBe('esc-002');
expect(response.hasMoreResults).toBe(true);
});
});

describe('formatResponse', () => {
it('formats empty results', () => {
expect(formatResponse({ results: [] })).toMatchInlineSnapshot(
`"No escalations found."`,
);
});

it('formats null response', () => {
expect(formatResponse(null)).toMatchInlineSnapshot(
`"No escalations found."`,
);
});

it('formats escalation results', () => {
const response = {
results: [
{
title: 'Test escalation',
status: 'OPEN',
priority: 'P1',
createdAt: '2026-04-20T08:30:00Z',
assignee: { name: 'Alice', email: 'alice@example.com' },
description: 'Something is broken.',
url: 'https://example.com/esc/1',
},
],
totalCount: 1,
hasMoreResults: false,
};

expect(formatResponse(response)).toMatchInlineSnapshot(`
"Found 1 escalation:

[1] Test escalation
Status: OPEN | Priority: P1
Assignee: Alice | Created: 4/20/2026
Something is broken.
URL: https://example.com/esc/1"
`);
});

it('indicates when more results are available', () => {
const response = {
results: [
{
title: 'Escalation A',
status: 'OPEN',
priority: 'P2',
},
],
totalCount: 50,
hasMoreResults: true,
};

const result = formatResponse(response);
expect(result).toContain('more results available');
expect(result).toContain('increase offset to paginate');
});

it('truncates long descriptions', () => {
const response = {
results: [
{
title: 'Verbose escalation',
status: 'OPEN',
priority: 'P1',
description: 'A'.repeat(300),
},
],
totalCount: 1,
hasMoreResults: false,
};

const result = formatResponse(response);
expect(result).toContain('...');
});
});
});
Loading
Loading