Skip to main content
Glama
e2e.test.ts23.7 kB
import { StdioClientTransport, StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js' import path from 'path' const RESPONSE_TIMEOUT = 1_000 // 1s const START_DELAY = 1_000 // 1s const TEST_TIMEOUT = 10_000 // 10s const SEARCH_DELAY = 8 // 10s const TOTAL_TOOLS = 111 const streamableClientUrl = new URL(`http://localhost:${process.env.PORT || 3000}/mcp`) jest.setTimeout(TEST_TIMEOUT) type ReadMessageType = { jsonrpc: string id: number result: { content?: { type: string text: string }[], tools?: any[] } } // In some tests the argument IDs in this object are modified, ensure the ID is always set per-test where relevant const jsonRpcMessage: Record<string, JSONRPCMessage> = { ping: { jsonrpc: "2.0", id: 1, method: "ping" }, pong: { jsonrpc: '2.0', id: 1, result: {} }, initialize: { jsonrpc: "2.0", id: 1, method: "initialize", params: { clientInfo: { name: "test-client", version: "1.0" }, protocolVersion: "2025-05-13", capabilities: {}, } }, toolsList: { jsonrpc: "2.0", id: 1, method: "tools/list" }, crmCreateCompany: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "crm_create_company", arguments: { properties: { name: "Test Company", domain: "test.com", website: "https://test.com", description: "Test Description", industry: "CONSUMER_SERVICES", numberofemployees: 10, annualrevenue: 100000, city: "Test City", state: "Test State", country: "Test Country", phone: "1234567890", address: "Test Address", address2: "Test Address 2", zip: "123456", type: "PARTNER", lifecyclestage: "lead" } } } }, crmGetCompany: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "crm_get_company", arguments: { companyId: "test-id" } } }, crmUpdateCompany: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "crm_update_company", arguments: { companyId: "test-id", properties: { name: "Test Company Updated" } } } }, crmSearchCompanies: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "crm_search_companies", arguments: { filterGroups: [{ filters: [{ propertyName: "name", operator: "CONTAINS_TOKEN", value: "Test Company" }] }] } } }, crmDeleteObject: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "crm_delete_object", arguments: { objectType: "companies", objectId: "test-id" } } }, productsCreate: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "products_create", arguments: { properties: { name: "Test Product" } } } }, productsRead: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "products_read", arguments: { productId: "test-id" } } }, productsUpdate: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "products_update", arguments: { productId: "test-id", properties: { name: "Test Product Updated" } } } }, productsList: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "products_list", arguments: { limit: 100, properties: ["name", "description"] } } }, productsSearch: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "products_search", arguments: { limit: 100, properties: ["name", "description"], filterGroups: [{ filters: [{ propertyName: "name", operator: "CONTAINS_TOKEN", value: "Test Product" }] }] } } }, productsArchive: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "products_archive", arguments: { productId: "test-id" } } }, productsBatchCreate: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "products_batch_create", arguments: { inputs: [ { properties: { name: "Test Product" } }, { properties: { name: "Test Product 2" } } ] } } }, productsBatchRead: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "products_batch_read", arguments: { propertiesWithHistory: [], properties: ["name"] } } }, productsBatchUpdate: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "products_batch_update", arguments: {} } }, productsBatchUpsert: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "products_batch_upsert", arguments: {} } }, productsBatchArchive: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "products_batch_archive", arguments: {} } } } const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) const sendPostRequest = async (message: JSONRPCMessage | JSONRPCMessage[], sessionId?: string) => ( fetch(streamableClientUrl, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...(sessionId ? { "mcp-session-id": sessionId } : {}), }, body: JSON.stringify(message), }) ) const getSSEData = async (response: Response) => { const reader = response.body?.getReader() let buffer = '' while (true) { const { value, done } = await reader!.read() if (done) break buffer += new TextDecoder().decode(value) const lines = buffer.split('\n') for (const line of lines) { if (line.startsWith('data:')) { try { return JSON.parse(line.slice(5).trim()) } catch (e) { // Ignore and continue accumulating } } } // Keep only the last (possibly incomplete) line in buffer buffer = lines[lines.length - 1] } throw new Error('No complete data line found') } describe('Hubspot MCP', () => { let stdioClient: StdioClientTransport let streamableClient: StreamableHTTPClientTransport beforeAll(async () => { const serverParameters: StdioServerParameters = { command: "node", args: [path.resolve(__dirname, '../dist/index.js')], env: process.env as Record<string, string> } stdioClient = new StdioClientTransport(serverParameters) await stdioClient.start() streamableClient = new StreamableHTTPClientTransport(streamableClientUrl) }) afterAll(async () => { await stdioClient.close() }) describe('Stdio Transport', () => { let readMessages: ReadMessageType[] let errors: Error[] let companyId: string let productId: string let batchProductIds: string[] beforeAll(async () => { await delay(START_DELAY) stdioClient.onmessage = (message) => readMessages.push(message as ReadMessageType) stdioClient.onerror = (error) => errors.push(error) }) beforeEach(async () => { readMessages = [] errors = [] }) it('responds to ping', async () => { stdioClient.send(jsonRpcMessage.ping) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0]).toEqual(jsonRpcMessage.pong) expect(errors).toHaveLength(0) }) it('returns a list of tools', async () => { stdioClient.send(jsonRpcMessage.toolsList) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.tools?.length).toEqual(TOTAL_TOOLS) }) it('can call the crm_create_company tool', async () => { stdioClient.send(jsonRpcMessage.crmCreateCompany) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) const company = JSON.parse(readMessages[0].result.content?.[0].text ?? '{}') expect(company.properties.name).toEqual('Test Company') companyId = company.id }) it('can call the crm_get_company tool', async () => { jsonRpcMessage.crmGetCompany["params"].arguments.companyId = companyId stdioClient.send(jsonRpcMessage.crmGetCompany) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) const company = JSON.parse(readMessages[0].result.content?.[0].text ?? '{}') expect(company.properties.name).toEqual('Test Company') }) it('can call the crm_update_company tool', async () => { jsonRpcMessage.crmUpdateCompany["params"].arguments.companyId = companyId stdioClient.send(jsonRpcMessage.crmUpdateCompany) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) const updated = JSON.parse(readMessages[0].result.content?.[0].text ?? '{}') expect(updated.properties.name).toEqual('Test Company Updated') }) it('can call the crm_search_companies tool', async () => { await delay(RESPONSE_TIMEOUT * SEARCH_DELAY) // Wait additional time to ensure the company is indexed stdioClient.send(jsonRpcMessage.crmSearchCompanies) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) const results = JSON.parse(readMessages[0].result.content?.[0].text ?? '{}') expect(Array.isArray(results.results)).toBe(true) expect(results.results.length).toBeGreaterThan(0) expect(results.results[0].id).toEqual(companyId) }) it('can call the crm_delete_object tool', async () => { jsonRpcMessage.crmDeleteObject["params"].arguments.objectId = companyId stdioClient.send(jsonRpcMessage.crmDeleteObject) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) expect(readMessages[0].result.content?.[0].text).toEqual(`No data returned: Status 204`) }) it('can call the products_create tool', async () => { stdioClient.send(jsonRpcMessage.productsCreate) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) const product = JSON.parse(readMessages[0].result.content?.[0].text ?? '{}') expect(product.properties.name).toEqual('Test Product') productId = product.id }) it('can call the products_read tool', async () => { jsonRpcMessage.productsRead["params"].arguments.productId = productId stdioClient.send(jsonRpcMessage.productsRead) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) const product = JSON.parse(readMessages[0].result.content?.[0].text ?? '{}') expect(product.properties.name).toEqual('Test Product') }) it('can call the products_update tool', async () => { jsonRpcMessage.productsUpdate["params"].arguments.productId = productId stdioClient.send(jsonRpcMessage.productsUpdate) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) const updated = JSON.parse(readMessages[0].result.content?.[0].text ?? '{}') expect(updated.properties.name).toEqual('Test Product Updated') }) it('can call the products_list tool', async () => { stdioClient.send(jsonRpcMessage.productsList) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) const results = JSON.parse(readMessages[0].result.content?.[0].text ?? '{}') expect(Array.isArray(results.results)).toBe(true) }) it('can call the products_search tool', async () => { await delay(RESPONSE_TIMEOUT * SEARCH_DELAY) // Wait additional time to ensure the product is indexed stdioClient.send(jsonRpcMessage.productsSearch) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) const results = JSON.parse(readMessages[0].result.content?.[0].text ?? '{}') expect(Array.isArray(results.results)).toBe(true) expect(results.results.length).toBeGreaterThan(0) expect(results.results[0].id).toEqual(productId) }) it('can call the products_archive tool', async () => { jsonRpcMessage.productsArchive["params"].arguments.productId = productId stdioClient.send(jsonRpcMessage.productsArchive) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) expect(readMessages[0].result.content?.[0].text).toEqual(`No data returned: Status 204`) }) it('can call the products_batch_create tool', async () => { stdioClient.send(jsonRpcMessage.productsBatchCreate) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) const result = JSON.parse(readMessages[0].result.content?.[0].text ?? '{}') expect(Array.isArray(result.results)).toBe(true) batchProductIds = result.results.map((p: any) => p.id) }) it('can call the products_batch_read tool', async () => { jsonRpcMessage.productsBatchRead["params"].arguments.inputs = batchProductIds.map(id => ({ id })) stdioClient.send(jsonRpcMessage.productsBatchRead) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) const result = JSON.parse(readMessages[0].result.content?.[0].text ?? '{}') expect(Array.isArray(result.results)).toBe(true) }) it('can call the products_batch_update tool', async () => { jsonRpcMessage.productsBatchUpdate["params"].arguments.inputs = batchProductIds.map(id => ({ id, properties: { name: `Batch Updated Product ${id}` } })) stdioClient.send(jsonRpcMessage.productsBatchUpdate) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) const result = JSON.parse(readMessages[0].result.content?.[0].text ?? '{}') expect(Array.isArray(result.results)).toBe(true) }) it('can call the products_batch_archive tool', async () => { jsonRpcMessage.productsBatchArchive["params"].arguments.productIds = batchProductIds stdioClient.send(jsonRpcMessage.productsBatchArchive) await delay(RESPONSE_TIMEOUT) expect(readMessages).toHaveLength(1) expect(readMessages[0].result.content?.length).toEqual(1) }) }) describe('Streamable HTTP Transport', () => { let companyId: string let productId: string let batchProductIds: string[] it('responds to ping', async () => { const response = await sendPostRequest(jsonRpcMessage.ping) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse).toEqual(jsonRpcMessage.pong) }) it('returns a list of tools', async () => { const response = await sendPostRequest(jsonRpcMessage.toolsList) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.tools?.length).toEqual(TOTAL_TOOLS) }) it('can call the crm_create_company tool', async () => { const response = await sendPostRequest(jsonRpcMessage.crmCreateCompany) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.length).toEqual(1) const company = JSON.parse(sseResponse.result.content?.[0].text ?? '{}') expect(company.properties.name).toEqual('Test Company') companyId = company.id }) it('can call the crm_get_company tool', async () => { jsonRpcMessage.crmGetCompany["params"].arguments.companyId = companyId const response = await sendPostRequest(jsonRpcMessage.crmGetCompany) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.length).toEqual(1) const company = JSON.parse(sseResponse.result.content?.[0].text ?? '{}') expect(company.properties.name).toEqual('Test Company') }) it('can call the crm_update_company tool', async () => { jsonRpcMessage.crmUpdateCompany["params"].arguments.companyId = companyId const response = await sendPostRequest(jsonRpcMessage.crmUpdateCompany) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.length).toEqual(1) const updated = JSON.parse(sseResponse.result.content?.[0].text ?? '{}') expect(updated.properties.name).toEqual('Test Company Updated') }) it('can call the crm_search_companies tool', async () => { await delay(RESPONSE_TIMEOUT * SEARCH_DELAY) // Wait additional time to ensure the company is indexed const response = await sendPostRequest(jsonRpcMessage.crmSearchCompanies) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.length).toEqual(1) const results = JSON.parse(sseResponse.result.content?.[0].text ?? '{}') expect(Array.isArray(results.results)).toBe(true) expect(results.results.length).toBeGreaterThan(0) expect(results.results[0].id).toEqual(companyId) }) it('can call the crm_delete_object tool', async () => { jsonRpcMessage.crmDeleteObject["params"].arguments.objectId = companyId const response = await sendPostRequest(jsonRpcMessage.crmDeleteObject) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.[0].text).toEqual(`No data returned: Status 204`) }) it('can call the products_create tool', async () => { const response = await sendPostRequest(jsonRpcMessage.productsCreate) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.length).toEqual(1) const product = JSON.parse(sseResponse.result.content?.[0].text ?? '{}') expect(product.properties.name).toEqual('Test Product') productId = product.id }) it('can call the products_read tool', async () => { jsonRpcMessage.productsRead["params"].arguments.productId = productId const response = await sendPostRequest(jsonRpcMessage.productsRead) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.length).toEqual(1) const product = JSON.parse(sseResponse.result.content?.[0].text ?? '{}') expect(product.properties.name).toEqual('Test Product') }) it('can call the products_update tool', async () => { jsonRpcMessage.productsUpdate["params"].arguments.productId = productId const response = await sendPostRequest(jsonRpcMessage.productsUpdate) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.length).toEqual(1) const updated = JSON.parse(sseResponse.result.content?.[0].text ?? '{}') expect(updated.properties.name).toEqual('Test Product Updated') }) it('can call the products_list tool', async () => { const response = await sendPostRequest(jsonRpcMessage.productsList) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.length).toEqual(1) const results = JSON.parse(sseResponse.result.content?.[0].text ?? '{}') expect(Array.isArray(results.results)).toBe(true) expect(results.results.length).toBeGreaterThan(0) expect(results.results[0].id).toEqual(productId) }) it('can call the products_search tool', async () => { await delay(RESPONSE_TIMEOUT * SEARCH_DELAY) // Wait additional time to ensure the product is indexed const response = await sendPostRequest(jsonRpcMessage.productsSearch) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.length).toEqual(1) const results = JSON.parse(sseResponse.result.content?.[0].text ?? '{}') expect(Array.isArray(results.results)).toBe(true) expect(results.results.length).toBeGreaterThan(0) expect(results.results[0].id).toEqual(productId) }) it('can call the products_archive tool', async () => { jsonRpcMessage.productsArchive["params"].arguments.productId = productId const response = await sendPostRequest(jsonRpcMessage.productsArchive) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.[0].text).toEqual(`No data returned: Status 204`) }) it('can call the products_batch_create tool', async () => { jsonRpcMessage.productsBatchCreate["params"].arguments.inputs = [ { properties: { name: "Test Product" } }, { properties: { name: "Test Product 2" } } ] const response = await sendPostRequest(jsonRpcMessage.productsBatchCreate) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.length).toEqual(1) const result = JSON.parse(sseResponse.result.content?.[0].text ?? '{}') expect(Array.isArray(result.results)).toBe(true) batchProductIds = result.results.map((p: any) => p.id) }) it('can call the products_batch_read tool', async () => { jsonRpcMessage.productsBatchRead["params"].arguments.inputs = batchProductIds.map(id => ({ id })) const response = await sendPostRequest(jsonRpcMessage.productsBatchRead) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.length).toEqual(1) const result = JSON.parse(sseResponse.result.content?.[0].text ?? '{}') expect(Array.isArray(result.results)).toBe(true) }) it('can call the products_batch_update tool', async () => { jsonRpcMessage.productsBatchUpdate["params"].arguments.inputs = batchProductIds.map(id => ({ id, properties: { name: `Batch Updated Product ${id}` } })) const response = await sendPostRequest(jsonRpcMessage.productsBatchUpdate) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.length).toEqual(1) const result = JSON.parse(sseResponse.result.content?.[0].text ?? '{}') expect(Array.isArray(result.results)).toBe(true) }) it('can call the products_batch_archive tool', async () => { jsonRpcMessage.productsBatchArchive["params"].arguments.productIds = batchProductIds const response = await sendPostRequest(jsonRpcMessage.productsBatchArchive) expect(response.status).toBe(200) const sseResponse = await getSSEData(response) expect(sseResponse.result.content?.[0].text).toEqual(`No data returned: Status 204`) }) }) })

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/BlackSand-Software/hubspot-mcp'

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