LiveStormMCPREADME.md10 matches
1# Livestorm API MCP Server
23This project creates a Model Context Protocol (MCP) server that wraps the
4Livestorm API, exposing:
56- GET endpoints as Resources
9## How it works
10111. The server fetches and parses the Livestorm API's OpenAPI definition
122. It dynamically creates MCP Resources and Tools based on the API endpoints
133. When a client requests a Resource or Tool, the server proxies the request to
14the Livestorm API
1516## MCP Definition
24{
25"mcpServers": {
26"livestorm-api": {
27"type": "streamable-http",
28"url": "https://supagroova--7fab7ae4322911f080e9569c3dd06744.web.val.run/mcp",
38{
39"mcpServers": {
40"livestorm-api": {
41"type": "sse",
42"serverUrl": "https://supagroova--7fab7ae4322911f080e9569c3dd06744.web.val.run/sse"
4950- `index.ts`: Main entry point with HTTP trigger
51- `livestorm.ts`: Functions to fetch and parse the OpenAPI definition
52- `mcp.ts`: MCP server setup and configuration
5367RUN_LOCAL=1
68PORT=8787
69LIVESTORM_API_TOKEN=your-livestorm-api-token-here
70```
7172This is useful for API tokens and local configuration.
7374You can run this MCP server locally using the [Deno](https://deno.land/)
10"security": [
11{
12"api_key": []
13},
14{
21"description": "Created",
22"content": {
23"application/vnd.api+json": {
24"schema": {
25"type": "object",
207},
208"examples": {
209"application/vnd.api+json": {
210"value": {
211"data": {
343"requestBody": {
344"content": {
345"application/vnd.api+json": {
346"schema": {
347"type": "object",
LiveStormMCPtypes.ts3 matches
12// Types for OpenAPI schema
3export interface OpenApiSchema {
4paths: Record<string, PathItem>;
5// Other OpenAPI properties we might need
6}
7
LiveStormMCPmcp.ts16 matches
6extractGetEndpoints,
7extractMutationEndpoints,
8fetchOpenApiSpec,
9proxyRequest,
10} from "../src/livestorm.ts";
2829/**
30* Sets up the MCP server with resources and tools based on the Livestorm API
31* Uses a cached instance if available
32*/
39// Create a new MCP server
40const server = new McpServer({
41name: "livestorm-api-server",
42version: "1.0.0",
43description: "MCP server that provides access to Livestorm API endpoints",
44}, { capabilities: { logging: {} } });
4546try {
47console.log("Fetching Livestorm API OpenAPI spec...");
48// Fetch the OpenAPI spec
49const openApiSpec = await fetchOpenApiSpec();
5051// Extract GET endpoints (Resources)
52const getEndpoints = extractGetEndpoints(openApiSpec);
5354// Extract mutation endpoints (Tools)
55const mutationEndpoints = extractMutationEndpoints(openApiSpec);
5657// Register Resources
127}
128129// Make the request to the Livestorm API
130const response = await proxyRequest(
131path,
137const errorText = await response.text();
138throw new Error(
139`Livestorm API error: ${response.status} ${response.statusText} - ${errorText}`,
140);
141}
236}
237238// Make the request to the Livestorm API
239const response = await proxyRequest(
240path,
247const errorText = await response.text();
248throw new Error(
249`Livestorm API error: ${response.status} ${response.statusText} - ${errorText}`,
250);
251}
283284/**
285* Creates a tool schema from an OpenAPI operation
286* Returns an object with input and output schemas using Zod
287*
314315const requestSchema = operation.requestBody
316?.content["application/vnd.api+json"]?.schema;
317const responseSchema = operation.responses["201"]
318?.content["application/vnd.api+json"]?.schema?.properties?.data.allOf[1];
319320// Iterate through the schema elements and add those to the inputSchema as Zod types
LiveStormMCPlivestorm.ts34 matches
1import "https://deno.land/std@0.224.0/dotenv/load.ts";
2import { parse as parseYaml } from "https://esm.sh/yaml@2.3.1";
3import { OpenApiSchema, OperationObject } from "./types.ts";
4import { blob } from "https://esm.town/v/std/blob";
5import { ValTownBlobNotFoundError } from "https://esm.town/v/std/ValTownBlobNotFoundError";
6import { ValTownBlobError } from "https://esm.town/v/std/ValTownBlobError";
78// URL of the Livestorm API OpenAPI definition
9const LIVESTORM_API_SPEC_URL =
10"https://api.livestorm.co/api-docs/v1/swagger.yaml";
11const LIVESTORM_API_BASE_URL = "https://api.livestorm.co/v1";
1213// Cache key for storing the OpenAPI spec in Blob storage
14const OPENAPI_SPEC_CACHE_KEY = "livestorm-openapi-spec";
1516/**
17* Fetches the Livestorm API OpenAPI definition
18*/
19export async function fetchOpenApiSpec(): Promise<OpenApiSchema> {
20try {
21// Check if the OpenAPI spec is cached in Blob storage
22let response;
23try {
24response = await blob.get(OPENAPI_SPEC_CACHE_KEY);
25console.log(`Using cached OpenAPI spec from Blob storage`);
26} catch (e) {
27if (
28e instanceof ValTownBlobNotFoundError || e instanceof ValTownBlobError
29) {
30console.log(`Fetching OpenAPI spec from ${LIVESTORM_API_SPEC_URL}...`);
31response = await fetch(LIVESTORM_API_SPEC_URL);
32if (!response.ok) {
33const errorText = await response.text();
34console.error(
35`Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}`,
36);
37console.error(`Response body: ${errorText}`);
38throw new Error(
39`Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}`,
40);
41}
4647const yamlText = await response.text();
48let parsedSpec: OpenApiSchema;
49try {
50parsedSpec = parseYaml(yamlText);
51// Validate the parsed spec has the expected structure
52if (!parsedSpec || !parsedSpec.paths) {
53throw new Error("Invalid OpenAPI spec: missing paths property");
54}
55} catch (parseError) {
56console.error("Error parsing OpenAPI spec YAML:", parseError);
57throw new Error(
58`Failed to parse OpenAPI spec: ${
59parseError instanceof Error ? parseError.message : String(parseError)
60}`,
62}
6364// Cache the OpenAPI spec in Blob storage
65try {
66await blob.set(OPENAPI_SPEC_CACHE_KEY, yamlText);
67} catch (e) {
68console.error("Error caching OpenAPI spec:", e);
69}
7071return parsedSpec;
72} catch (error) {
73console.error("Error fetching OpenAPI spec:", error);
74throw error;
75}
7778/**
79* Extracts GET endpoints from the OpenAPI spec
80*/
81export function extractGetEndpoints(spec: OpenApiSchema) {
82const getEndpoints: Record<
83string,
99100/**
101* Extracts POST, PUT, DELETE endpoints from the OpenAPI spec
102*/
103export function extractMutationEndpoints(spec: OpenApiSchema) {
104const mutationEndpoints: Record<
105string,
148149/**
150* Proxies a request to the Livestorm API
151*/
152export async function proxyRequest(
157): Promise<Response> {
158// Replace path parameters in the URL
159let url = `${LIVESTORM_API_BASE_URL}${path}`;
160161// Extract path parameters and replace them in the URL
193194// Forward the Authorization header
195const authHeader = Deno.env.get("LIVESTORM_API_TOKEN");
196if (!authHeader) {
197throw new Error(
198"Authorization header is required for Livestorm API requests",
199);
200}
222223try {
224// Make the request to Livestorm API
225const response = await fetch(url, requestOptions);
226230console.error(`Error proxying request to ${url}:`, error);
231throw new Error(
232`Failed to proxy request to Livestorm API: ${
233error instanceof Error ? error.message : String(error)
234}`,
27});
2829// API endpoint for contact form submissions (example)
30app.post("/api/contact", async c => {
31try {
32const formData = await c.req.json();
51});
5253// API endpoint for get involved form submissions (example)
54app.post("/api/get-involved", async c => {
55try {
56const formData = await c.req.json();
7172// Health check endpoint
73app.get("/api/health", c => {
74return c.json({ status: "ok", timestamp: new Date().toISOString() });
75});
untitled-3016index.ts3 matches
15await runMigrations();
1617// API routes
18app.route("/api/jobs", jobsRouter);
19app.route("/api/chat", chatRouter);
2021// Serve static files
untitled-3016ChatRoom.tsx5 matches
1/** @jsxImportSource https://esm.sh/react@18.2.0 */
2import React, { useState, useEffect, useRef } from "https://esm.sh/react@18.2.0?deps=react@18.2.0";
3import type { ChatMessage, ApiResponse } from "../../shared/types.ts";
45export default function ChatRoom() {
19try {
20setLoading(true);
21const response = await fetch('/api/chat/messages');
22const result: ApiResponse<ChatMessage[]> = await response.json();
23
24if (result.success && result.data) {
4647try {
48const response = await fetch('/api/chat/messages', {
49method: 'POST',
50headers: {
57});
5859const result: ApiResponse<ChatMessage> = await response.json();
6061if (result.success && result.data) {
untitled-3016JobForm.tsx3 matches
1/** @jsxImportSource https://esm.sh/react@18.2.0 */
2import React, { useState } from "https://esm.sh/react@18.2.0?deps=react@18.2.0";
3import type { Job, ApiResponse } from "../../shared/types.ts";
45interface JobFormProps {
2526try {
27const response = await fetch('/api/jobs', {
28method: 'POST',
29headers: {
33});
3435const result: ApiResponse<Job> = await response.json();
3637if (result.success && result.data) {
untitled-3016JobBoard.tsx5 matches
1/** @jsxImportSource https://esm.sh/react@18.2.0 */
2import React, { useState, useEffect } from "https://esm.sh/react@18.2.0?deps=react@18.2.0";
3import type { Job, ApiResponse } from "../../shared/types.ts";
4import JobForm from "./JobForm.tsx";
513try {
14setLoading(true);
15const response = await fetch('/api/jobs');
16const result: ApiResponse<Job[]> = await response.json();
17
18if (result.success && result.data) {
4041try {
42const response = await fetch(`/api/jobs/${jobId}`, {
43method: 'DELETE'
44});
45const result: ApiResponse<boolean> = await response.json();
46
47if (result.success) {