Skip to main content
Glama

generate-user-stories

Creates detailed user stories with acceptance criteria from a product description, aiding in software development planning and requirements specification.

Instructions

Creates detailed user stories with acceptance criteria based on a product description and research.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
productDescriptionYesDescription of the product to create user stories for

Implementation Reference

  • Main handler/executor function that performs pre-generation research using Perplexity, generates user stories via LLM with a specific system prompt, saves the markdown output to a file, and handles background job management with SSE notifications.
    export const generateUserStories: ToolExecutor = async (
      params: Record<string, unknown>,
      config: OpenRouterConfig,
      context?: ToolExecutionContext
    ): Promise<CallToolResult> => {
      const sessionId = context?.sessionId || 'unknown-session';
      if (sessionId === 'unknown-session') {
          logger.warn({ tool: 'generateUserStories' }, 'Executing tool without a valid sessionId. SSE progress updates will not be sent.');
      }
    
      logger.debug({
        configReceived: true,
        hasLlmMapping: Boolean(config.llm_mapping),
        mappingKeys: config.llm_mapping ? Object.keys(config.llm_mapping) : []
      }, 'generateUserStories executor received config');
    
      const productDescription = params.productDescription as string;
    
      const jobId = jobManager.createJob('generate-user-stories', params);
      logger.info({ jobId, tool: 'generateUserStories', sessionId }, 'Starting background job.');
    
      const initialResponse = formatBackgroundJobInitiationResponse(
        jobId,
        'generate-user-stories',
        'User Stories Generator'
      );
    
      setImmediate(async () => {
        const logs: string[] = [];
        let filePath: string = '';
    
        try {
          jobManager.updateJobStatus(jobId, JobStatus.RUNNING, 'Starting user stories generation process...');
          sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Starting user stories generation process...');
          logs.push(`[${new Date().toISOString()}] Starting user stories generation for: ${productDescription.substring(0, 50)}...`);
    
          await initDirectories(context);
    
          const userStoriesDir = path.join(getBaseOutputDir(context), 'user-stories-generator');
          const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
          const sanitizedName = productDescription.substring(0, 30).toLowerCase().replace(/[^a-z0-9]+/g, '-');
          const filename = `${timestamp}-${sanitizedName}-user-stories.md`;
          filePath = path.join(userStoriesDir, filename);
    
          logger.info({ jobId, inputs: { productDescription: productDescription.substring(0, 50) } }, "User Stories Generator: Starting pre-generation research...");
          jobManager.updateJobStatus(jobId, JobStatus.RUNNING, 'Performing pre-generation research...');
          sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Performing pre-generation research...');
          logs.push(`[${new Date().toISOString()}] Starting pre-generation research.`);
    
          let researchContext = '';
        try {
          const query1 = `User personas and stakeholders for: ${productDescription}`;
          const query2 = `Common user workflows and use cases for: ${productDescription}`;
          const query3 = `User experience expectations and pain points for: ${productDescription}`;
    
          const researchResults = await Promise.allSettled([
            performResearchQuery(query1, config),
            performResearchQuery(query2, config),
            performResearchQuery(query3, config)
          ]);
    
          researchContext = "## Pre-Generation Research Context (From Perplexity Sonar Deep Research):\n\n";
    
          researchResults.forEach((result, index) => {
            const queryLabels = ["User Personas & Stakeholders", "User Workflows & Use Cases", "User Experience Expectations & Pain Points"];
            if (result.status === "fulfilled") {
              researchContext += `### ${queryLabels[index]}:\n${result.value.trim()}\n\n`;
            } else {
              logger.warn({ error: result.reason }, `Research query ${index + 1} failed`);
              researchContext += `### ${queryLabels[index]}:\n*Research on this topic failed.*\n\n`;
            }
          });
    
          logger.info({ jobId }, "User Stories Generator: Pre-generation research completed.");
          jobManager.updateJobStatus(jobId, JobStatus.RUNNING, 'Research complete. Starting main user stories generation...');
          sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Research complete. Starting main user stories generation...');
          logs.push(`[${new Date().toISOString()}] Pre-generation research completed.`);
    
        } catch (researchError) {
          logger.error({ jobId, err: researchError }, "User Stories Generator: Error during research aggregation");
          logs.push(`[${new Date().toISOString()}] Error during research aggregation: ${researchError instanceof Error ? researchError.message : String(researchError)}`);
          researchContext = "## Pre-Generation Research Context:\n*Error occurred during research phase.*\n\n";
          sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Warning: Error during research phase. Continuing generation...');
        }
    
        const mainGenerationPrompt = `Create comprehensive user stories for the following product:\n\n${productDescription}\n\n${researchContext}`;
    
        logger.info({ jobId }, "User Stories Generator: Starting main generation using direct LLM call...");
        jobManager.updateJobStatus(jobId, JobStatus.RUNNING, 'Generating user stories content via LLM...');
        sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Generating user stories content via LLM...');
        logs.push(`[${new Date().toISOString()}] Calling LLM for main user stories generation.`);
    
        const userStoriesMarkdown = await performFormatAwareLlmCallWithCentralizedConfig(
          mainGenerationPrompt,
          USER_STORIES_SYSTEM_PROMPT,
          'user_stories_generation',
          'markdown', // Explicitly specify markdown format
          undefined, // No schema for markdown
          0.3
        );
    
        logger.info({ jobId }, "User Stories Generator: Main generation completed.");
        jobManager.updateJobStatus(jobId, JobStatus.RUNNING, 'Processing LLM response...');
        sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Processing LLM response...');
        logs.push(`[${new Date().toISOString()}] Received response from LLM.`);
    
        if (!userStoriesMarkdown || typeof userStoriesMarkdown !== 'string' || !userStoriesMarkdown.trim().startsWith('# User Stories:')) {
          logger.warn({ jobId, markdown: userStoriesMarkdown?.substring(0, 100) }, 'User stories generation returned empty or potentially invalid Markdown format.');
          logs.push(`[${new Date().toISOString()}] Validation Error: LLM output invalid format.`);
          throw new ToolExecutionError('User stories generation returned empty or invalid Markdown content.');
        }
    
        const formattedResult = `${userStoriesMarkdown}\n\n_Generated: ${new Date().toLocaleString()}_`;
    
        logger.info({ jobId }, `Saving user stories to ${filePath}...`);
        jobManager.updateJobStatus(jobId, JobStatus.RUNNING, `Saving user stories to file...`);
        sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, `Saving user stories to file...`);
        logs.push(`[${new Date().toISOString()}] Saving user stories to ${filePath}.`);
    
        await fs.writeFile(filePath, formattedResult, 'utf8');
        logger.info({ jobId }, `User stories generated and saved to ${filePath}`);
        logs.push(`[${new Date().toISOString()}] User stories saved successfully.`);
        sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, `User stories saved successfully.`);
    
        const finalResult: CallToolResult = {
          content: [{ type: "text", text: `User stories generated successfully and saved to: ${filePath}\n\n${formattedResult}` }],
          isError: false
        };
        jobManager.setJobResult(jobId, finalResult);
    
        } catch (error) {
          const errorMsg = error instanceof Error ? error.message : String(error);
          logger.error({ err: error, jobId, tool: 'generate-user-stories', params }, `User Stories Generator Error: ${errorMsg}`);
          logs.push(`[${new Date().toISOString()}] Error: ${errorMsg}`);
    
          let appError: AppError;
          const cause = error instanceof Error ? error : undefined;
          if (error instanceof AppError) {
            appError = error;
          } else {
            appError = new ToolExecutionError(`Failed to generate user stories: ${errorMsg}`, { params, filePath }, cause);
          }
    
          const mcpError = new McpError(ErrorCode.InternalError, appError.message, appError.context);
          const errorResult: CallToolResult = {
            content: [{ type: 'text', text: `Error during background job ${jobId}: ${mcpError.message}\n\nLogs:\n${logs.join('\n')}` }],
            isError: true,
            errorDetails: mcpError
          };
    
          jobManager.setJobResult(jobId, errorResult);
          sseNotifier.sendProgress(sessionId, jobId, JobStatus.FAILED, `Job failed: ${mcpError.message}`);
        }
      });
    
      return initialResponse;
    };
  • Zod schema defining the input parameters for the tool (productDescription).
    const userStoriesInputSchemaShape = {
      productDescription: z.string().min(10, { message: "Product description must be at least 10 characters." }).describe("Description of the product to create user stories for")
    };
  • Tool definition object and call to registerTool, which adds it to the dynamic registry used by server.ts.
    const userStoriesToolDefinition: ToolDefinition = {
      name: "user-stories-generator",
      description: "Creates detailed user stories with acceptance criteria based on a product description and research.",
      inputSchema: userStoriesInputSchemaShape,
      executor: generateUserStories
    };
    
    registerTool(userStoriesToolDefinition);
  • Detailed system prompt for the LLM to generate well-structured user stories in Markdown format, incorporating research context.
    const USER_STORIES_SYSTEM_PROMPT = `
    # User Stories Generator - Using Research Context
    
    # ROLE & GOAL
    You are an expert Agile Business Analyst and Product Owner AI assistant. Your goal is to generate a comprehensive and well-structured set of User Stories, including Epics and Acceptance Criteria, in Markdown format.
    
    # CORE TASK
    Generate detailed user stories based on the user's product description and the provided research context.
    
    # INPUT HANDLING
    - Analyze the 'productDescription' to understand the product's purpose, core features, and intended value.
    - You will also receive 'Pre-Generation Research Context'.
    
    # RESEARCH CONTEXT INTEGRATION
    - **CRITICAL:** Carefully review the '## Pre-Generation Research Context (From Perplexity Sonar Deep Research)' section provided in the user prompt.
    - This section contains insights on: User Personas & Stakeholders, User Workflows & Use Cases, and User Experience Expectations & Pain Points.
    - **Use these insights** heavily to:
        - Define realistic 'As a [user type/persona]' roles based on the research.
        - Create stories that address identified 'User Workflows & Use Cases'.
        - Ensure stories align with 'User Experience Expectations' and address 'Pain Points'.
        - Inform the 'Priority' and 'Value/Benefit' parts of the stories.
    - **Synthesize**, don't just list research findings. Create user stories that *embody* the research.
    
    # OUTPUT FORMAT & STRUCTURE (Strict Markdown)
    - Your entire response **MUST** be valid Markdown.
    - Start **directly** with the main title: '# User Stories: [Inferred Product Name]'
    - Organize stories hierarchically using Markdown headings:
        - \`## Epic: [Epic Title]\` (e.g., \`## Epic: User Authentication\`)
        - \`### User Story: [Story Title]\` (e.g., \`### User Story: User Registration\`)
    - For **each User Story**, use the following precise template within its \`###\` section:
    
      **ID:** US-[auto-incrementing number, e.g., US-101]
      **Title:** [Concise Story Title]
    
      **Story:**
      > As a **[User Role/Persona - informed by research]**,
      > I want to **[perform an action or achieve a goal]**
      > So that **[I gain a specific benefit - linked to user needs/pain points from research]**.
    
      **Acceptance Criteria:**
      *   GIVEN [precondition/context] WHEN [action is performed] THEN [expected, testable outcome].
      *   GIVEN [another context] WHEN [different action] THEN [another outcome].
      *   *(Provide multiple, specific, measurable criteria)*
    
      **Priority:** [High | Medium | Low - informed by perceived value/dependencies/research]
      **Dependencies:** [List of User Story IDs this depends on, e.g., US-100 | None]
      **(Optional) Notes:** [Any clarifying details or technical considerations.]
    
    # QUALITY ATTRIBUTES
    - **INVEST Principles:** Ensure stories are Independent, Negotiable, Valuable, Estimable, Small (appropriately sized), and Testable (via Acceptance Criteria).
    - **User-Centric:** Focus on user roles, actions, and benefits, informed by research personas and needs.
    - **Clear Acceptance Criteria:** Criteria must be specific, unambiguous, and testable.
    - **Comprehensive:** Cover the core functionality implied by the description and research workflows.
    - **Well-Structured:** Adhere strictly to the Epic/Story hierarchy and template format.
    - **Consistent:** Use consistent terminology and formatting.
    
    # CONSTRAINTS (Do NOT Do the Following)
    - **NO Conversational Filler:** Start directly with the '# User Stories: ...' title. No intros, summaries, or closings.
    - **NO Markdown Violations:** Strictly adhere to the specified Markdown format (headings, blockquotes for the story, lists for AC).
    - **NO Implementation Details:** Focus on *what* the user needs, not *how* it will be built (unless specified in 'Notes').
    - **NO External Knowledge:** Base stories *only* on the provided inputs and research context.
    - **NO Process Commentary:** Do not mention the research process in the output.
    - **Strict Formatting:** Use \`##\` for Epics, \`###\` for Stories. Use the exact field names (ID, Title, Story, Acceptance Criteria, etc.) in bold. Use Markdown blockquotes for the As a/I want/So that structure.
    `;
Install Server

Other Tools

Related Tools

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/freshtechbro/vibe-coder-mcp'

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