Skip to main content
Glama

Goal Story MCP Server

by hichana
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import axios from "axios"; import { CAPTURE_MESSAGES, CLARIFY_MESSAGES, CONTEXT_MESSAGES, DISCUSS_MESSAGES, FOMRULATE_MESSAGES, MANAGE_MESSAGES, PROMPTS, VISUALIZE_MESSAGES, } from "./prompts.js"; import { ABOUT_GOAL_STORYING } from "./resources.js"; import { ABOUT_GOALSTORYING_TOOL, COUNT_GOALS_TOOL, CREATE_GOAL_TOOL, CREATE_SCHEDULED_STORY_TOOL, CREATE_STEPS_TOOL, CREATE_STORY_TOOL, DESTROY_GOAL_TOOL, DESTROY_SCHEDULED_STORY_TOOL, DESTROY_STEP_TOOL, GET_STORY_CONTEXT_TOOL, READ_CURRENT_FOCUS_TOOL, READ_GOALS_TOOL, READ_ONE_GOAL_TOOL, READ_ONE_STEP_TOOL, READ_ONE_STORY_TOOL, READ_SCHEDULED_STORIES_TOOL, READ_SELF_USER_TOOL, READ_STEPS_TOOL, READ_STORIES_TOOL, SET_STEPS_ORDER_TOOL, UPDATE_GOAL_TOOL, UPDATE_SCHEDULED_STORY_TOOL, UPDATE_SELF_USER_TOOL, UPDATE_STEP_NOTES_TOOL, UPDATE_STEP_TOOL, } from "./tools.js"; // ----------------------------------------- // Environment variables & basic setup // ----------------------------------------- const argv = process.argv.slice(2); const GOALSTORY_API_BASE_URL = argv[0]; const GOALSTORY_API_TOKEN = argv[1]; if (!GOALSTORY_API_BASE_URL) { console.error("Error: GOALSTORY_API_BASE_URL argument is required"); process.exit(1); } if (!GOALSTORY_API_TOKEN) { console.error("Error: GOALSTORY_API_TOKEN argument is required"); process.exit(1); } // Helper to make HTTP requests async function doRequest<T = any>( url: string, method: string, body?: unknown, ): Promise<T> { console.error("Making request to:", url); console.error("Method:", method); console.error("Body:", body ? JSON.stringify(body) : "none"); try { const response = await axios({ url, method, headers: { "Content-Type": "application/json", Authorization: `Bearer ${GOALSTORY_API_TOKEN}`, }, data: body, timeout: 10000, // 10 second timeout validateStatus: function (status) { return status >= 200 && status < 500; // Accept all status codes less than 500 }, }); console.error("Response received:", response.status); return response.data as T; } catch (err) { console.error("Request failed with error:", err); if (axios.isAxiosError(err)) { if (err.code === "ECONNABORTED") { throw new Error( `Request timed out after 10 seconds. URL: ${url}, Method: ${method}`, ); } if (err.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx throw new Error( `HTTP Error ${ err.response.status }. URL: ${url}, Method: ${method}, Body: ${JSON.stringify( body, )}. Error text: ${JSON.stringify(err.response.data)}`, ); } else if (err.request) { // The request was made but no response was received throw new Error( `No response received from server. URL: ${url}, Method: ${method}`, ); } else { // Something happened in setting up the request that triggered an Error throw new Error(`Request setup failed: ${err.message}`); } } else { // Something else happened throw new Error( `Unexpected error: ${err instanceof Error ? err.message : String(err)}`, ); } } } // ----------------------------------------- // MCP server // ----------------------------------------- const server = new McpServer( { name: "goalstory-mcp-server", version: "0.4.6", }, { capabilities: { tools: {}, resources: {}, prompts: {}, }, }, ); // ----------------------------------------- // Define Tools as server.tool invocations // ----------------------------------------- /** * About GoalStorying */ server.tool( ABOUT_GOALSTORYING_TOOL.name, ABOUT_GOALSTORYING_TOOL.description, ABOUT_GOALSTORYING_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/about`; const result = await doRequest(url, "GET"); return { content: [ { type: "text", text: `About data:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Read Self User */ server.tool( READ_SELF_USER_TOOL.name, READ_SELF_USER_TOOL.description, READ_SELF_USER_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/users`; const result = await doRequest(url, "GET"); return { content: [ { type: "text", text: `User data:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Update Self User */ server.tool( UPDATE_SELF_USER_TOOL.name, UPDATE_SELF_USER_TOOL.description, UPDATE_SELF_USER_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/users`; const body = { ...(args.name ? { name: args.name } : {}), ...(args.about ? { about: args.about } : {}), ...(typeof args.visibility === "number" ? { visibility: args.visibility, } : {}), }; const result = await doRequest(url, "PATCH", body); return { content: [ { type: "text", text: `Updated user:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Count Goals */ server.tool( COUNT_GOALS_TOOL.name, COUNT_GOALS_TOOL.description, COUNT_GOALS_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/count/goals`; const result = await doRequest(url, "GET"); return { content: [ { type: "text", text: `Count of goals:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Create Goal */ server.tool( CREATE_GOAL_TOOL.name, CREATE_GOAL_TOOL.description, CREATE_GOAL_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/goals`; const body = { name: args.name, ...(args.description ? { description: args.description, } : {}), ...(args.story_mode ? { story_mode: args.story_mode, } : {}), ...(args.belief_mode ? { belief_mode: args.belief_mode, } : {}), }; const result = await doRequest(url, "POST", body); return { content: [ { type: "text", text: `Goal created:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Update Goal */ server.tool( UPDATE_GOAL_TOOL.name, UPDATE_GOAL_TOOL.description, UPDATE_GOAL_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/goals/${args.id}`; const body = { id: args.id, ...(args.name ? { name: args.name } : {}), ...(typeof args.status === "number" ? { status: args.status } : {}), ...(args.description ? { description: args.description, } : {}), ...(args.outcome ? { outcome: args.outcome } : {}), ...(args.evidence ? { evidence: args.evidence } : {}), ...(args.story_mode ? { story_mode: args.story_mode, } : {}), ...(args.belief_mode ? { belief_mode: args.belief_mode, } : {}), }; const result = await doRequest(url, "PATCH", body); return { content: [ { type: "text", text: `Goal updated:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Destroy Goal */ server.tool( DESTROY_GOAL_TOOL.name, DESTROY_GOAL_TOOL.description, DESTROY_GOAL_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/goals/${args.id}`; const result = await doRequest(url, "DELETE"); return { content: [ { type: "text", text: `Goal deleted:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Read One Goal */ server.tool( READ_ONE_GOAL_TOOL.name, READ_ONE_GOAL_TOOL.description, READ_ONE_GOAL_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/goals/${args.id}`; const result = await doRequest(url, "GET"); return { content: [ { type: "text", text: `Goal data:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Read Goals */ server.tool( READ_GOALS_TOOL.name, READ_GOALS_TOOL.description, READ_GOALS_TOOL.inputSchema.shape, async (args) => { const params = new URLSearchParams(); if (args.page) params.set("page", `${args.page}`); if (args.limit) params.set("limit", `${args.limit}`); const url = `${GOALSTORY_API_BASE_URL}/goals?${params.toString()}`; const result = await doRequest(url, "GET"); return { content: [ { type: "text", text: `Goals retrieved:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Read Current Focus */ server.tool( READ_CURRENT_FOCUS_TOOL.name, READ_CURRENT_FOCUS_TOOL.description, READ_CURRENT_FOCUS_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/current`; const result = await doRequest(url, "GET"); return { content: [ { type: "text", text: `Current goal/step focus:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Get Story Context */ server.tool( GET_STORY_CONTEXT_TOOL.name, GET_STORY_CONTEXT_TOOL.description, GET_STORY_CONTEXT_TOOL.inputSchema.shape, async (args) => { const params = new URLSearchParams(); params.set("goalId", args.goalId); params.set("stepId", args.stepId); if (args.feedback) params.set("feedback", args.feedback); const url = `${GOALSTORY_API_BASE_URL}/context?${params.toString()}`; const result = await doRequest(url, "GET"); return { content: [ { type: "text", text: `Story context:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Create Steps */ server.tool( CREATE_STEPS_TOOL.name, CREATE_STEPS_TOOL.description, CREATE_STEPS_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/steps`; // when developing locally, we can pass in a list of strings in the MCP // inspector like this: step1, step2 let steps = args.steps; if (typeof steps === "string") { const itemsAreAString = steps as string; steps = itemsAreAString .split(",") .map((s) => s.trim()) .filter((s) => s.length > 0); } const body = { goal_id: args.goal_id, steps, }; const result = await doRequest(url, "POST", body); return { content: [ { type: "text", text: `Steps created:\n${JSON.stringify(result, null, 2)}\n\nNOTE: Steps are ordered by their 'order_ts' timestamp in ascending order - the step with the smallest timestamp value (updated first) is step 1. The steps appear in order, with the first step having the smallest timestamp. Example: If step A has timestamp 12:00 and step B has timestamp 12:01, then step A is step 1 and step B is step 2.`, }, ], isError: false, }; }, ); /** * Read Steps */ server.tool( READ_STEPS_TOOL.name, READ_STEPS_TOOL.description, READ_STEPS_TOOL.inputSchema.shape, async (args) => { const params = new URLSearchParams(); params.set("goal_id", args.goal_id); if (args.page) params.set("page", `${args.page}`); if (args.limit) params.set("limit", `${args.limit}`); const url = `${GOALSTORY_API_BASE_URL}/steps?${params.toString()}`; const result = await doRequest(url, "GET"); return { content: [ { type: "text", text: `Steps for goal '${args.goal_id}':\n${JSON.stringify( result, null, 2, )}\n\nIMPORTANT: Steps are ordered by their 'order_ts' timestamp in ascending order - the step with the smallest timestamp value (updated first) is step 1, and steps with larger timestamp values come later in the sequence. Example: If step A has timestamp 12:00 and step B has timestamp 12:01, then step A is step 1 and step B is step 2.`, }, ], isError: false, }; }, ); /** * Read One Step */ server.tool( READ_ONE_STEP_TOOL.name, READ_ONE_STEP_TOOL.description, READ_ONE_STEP_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/steps/${args.id}`; const result = await doRequest(url, "GET"); return { content: [ { type: "text", text: `Step data:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Update Step */ server.tool( UPDATE_STEP_TOOL.name, UPDATE_STEP_TOOL.description, UPDATE_STEP_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/steps/${args.id}`; const body = { id: args.id, ...(args.name ? { name: args.name } : {}), ...(typeof args.status === "number" ? { status: args.status } : {}), ...(args.outcome ? { outcome: args.outcome } : {}), ...(args.evidence ? { evidence: args.evidence } : {}), }; const result = await doRequest(url, "PATCH", body); return { content: [ { type: "text", text: `Step updated:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Destroy Step */ server.tool( DESTROY_STEP_TOOL.name, DESTROY_STEP_TOOL.description, DESTROY_STEP_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/steps/${args.id}`; const result = await doRequest(url, "DELETE"); return { content: [ { type: "text", text: `Step deleted:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Update Step Notes */ server.tool( UPDATE_STEP_NOTES_TOOL.name, UPDATE_STEP_NOTES_TOOL.description, UPDATE_STEP_NOTES_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/step/notes/${args.id}`; const body = { id: args.id, notes: args.notes, }; const result = await doRequest(url, "PATCH", body); return { content: [ { type: "text", text: `Step notes updated:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Set Steps Order */ server.tool( SET_STEPS_ORDER_TOOL.name, SET_STEPS_ORDER_TOOL.description, SET_STEPS_ORDER_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/steps/order`; // If ordered_steps_ids comes in as a string (for local development), convert it to array let ordered_steps_ids = args.ordered_steps_ids; if (typeof ordered_steps_ids === "string") { const idsAsString = ordered_steps_ids as string; ordered_steps_ids = idsAsString .split(",") .map((s) => s.trim()) .filter((s) => s.length > 0); } const body = { ordered_steps_ids, }; const result = await doRequest(url, "POST", body); return { content: [ { type: "text", text: `Steps order updated:\n${JSON.stringify(result, null, 2)}\n\nIMPORTANT: The first step in the array has the smallest 'order_ts' timestamp (step 1), and each subsequent step has progressively larger timestamps that determine their order in the sequence. Example: If step A has timestamp 12:00 and step B has timestamp 12:01, then step A is step 1 and step B is step 2.`, }, ], isError: false, }; }, ); /** * Create Story */ server.tool( CREATE_STORY_TOOL.name, CREATE_STORY_TOOL.description, CREATE_STORY_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/stories`; const body = { goal_id: args.goal_id, step_id: args.step_id, title: args.title, story_text: args.story_text, }; const result = await doRequest(url, "POST", body); return { content: [ { type: "text", text: `Story created:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Read Stories */ server.tool( READ_STORIES_TOOL.name, READ_STORIES_TOOL.description, READ_STORIES_TOOL.inputSchema.shape, async (args) => { const params = new URLSearchParams(); params.set("goal_id", args.goal_id); params.set("step_id", args.step_id); if (args.page) params.set("page", `${args.page}`); if (args.limit) params.set("limit", `${args.limit}`); const url = `${GOALSTORY_API_BASE_URL}/stories?${params.toString()}`; const result = await doRequest(url, "GET"); return { content: [ { type: "text", text: `Stories:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Read One Story */ server.tool( READ_ONE_STORY_TOOL.name, READ_ONE_STORY_TOOL.description, READ_ONE_STORY_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/stories/${args.id}`; const result = await doRequest(url, "GET"); return { content: [ { type: "text", text: `Story data:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Read Scheduled Stories */ server.tool( READ_SCHEDULED_STORIES_TOOL.name, READ_SCHEDULED_STORIES_TOOL.description, READ_SCHEDULED_STORIES_TOOL.inputSchema.shape, async (args) => { const params = new URLSearchParams(); if (args.page) params.set("page", `${args.page}`); if (args.limit) params.set("limit", `${args.limit}`); const url = `${GOALSTORY_API_BASE_URL}/schedules/stories?${params.toString()}`; const result = await doRequest(url, "GET"); return { content: [ { type: "text", text: `Scheduled stories retrieved:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Create Scheduled Story */ server.tool( CREATE_SCHEDULED_STORY_TOOL.name, CREATE_SCHEDULED_STORY_TOOL.description, CREATE_SCHEDULED_STORY_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/schedules/stories`; const body = { goal_id: args.goal_id, timeSettings: args.timeSettings, }; const result = await doRequest(url, "POST", body); return { content: [ { type: "text", text: `Scheduled story created:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Update Scheduled Story */ server.tool( UPDATE_SCHEDULED_STORY_TOOL.name, UPDATE_SCHEDULED_STORY_TOOL.description, UPDATE_SCHEDULED_STORY_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/schedules/stories/${args.id}`; const body = { id: args.id, ...(args.timeSettings ? { timeSettings: args.timeSettings } : {}), ...(typeof args.status === "number" ? { status: args.status } : {}), }; const result = await doRequest(url, "PATCH", body); return { content: [ { type: "text", text: `Scheduled story updated:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); /** * Destroy Scheduled Story */ server.tool( DESTROY_SCHEDULED_STORY_TOOL.name, DESTROY_SCHEDULED_STORY_TOOL.description, DESTROY_SCHEDULED_STORY_TOOL.inputSchema.shape, async (args) => { const url = `${GOALSTORY_API_BASE_URL}/schedules/stories/${args.id}`; const result = await doRequest(url, "DELETE"); return { content: [ { type: "text", text: `Scheduled story deleted:\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; }, ); // ----------------------------------------- // RESOURCES // ----------------------------------------- const ABOUT_GOALSTORY_RESOURCE_URI = `file:///docs/about-goalstory.md`; // List available resources server.resource( "About Goal Story", ABOUT_GOALSTORY_RESOURCE_URI, async (uri) => ({ contents: [ { uri: uri.href, text: ABOUT_GOAL_STORYING, }, ], }), ); // ----------------------------------------- // PROMPTS // ----------------------------------------- server.prompt(PROMPTS.CLARIFY, {}, () => ({ messages: CLARIFY_MESSAGES, })); server.prompt(PROMPTS.FORMULATE, {}, () => ({ messages: FOMRULATE_MESSAGES, })); server.prompt(PROMPTS.CONTEXT, {}, () => ({ messages: CONTEXT_MESSAGES, })); server.prompt(PROMPTS.DISCUSS, {}, () => ({ messages: DISCUSS_MESSAGES, })); server.prompt(PROMPTS.CAPTURE, {}, () => ({ messages: CAPTURE_MESSAGES, })); server.prompt(PROMPTS.VISUALIZE, {}, () => ({ messages: VISUALIZE_MESSAGES, })); server.prompt(PROMPTS.MANAGE, {}, () => ({ messages: MANAGE_MESSAGES, })); // ----------------------------------------- // Running the server // ----------------------------------------- async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("GoalStory MCP Server running on stdio"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); });

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/hichana/goalstory-mcp'

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