Skip to main content
Glama
MissionSquad

MCP Avantage

by MissionSquad

technicalIndicators_ema

Calculate Exponential Moving Average (EMA) for financial assets to identify trends and generate trading signals using stock symbols, time intervals, and price data.

Instructions

Exponential Moving Average (EMA)

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
symbolYesThe stock symbol (e.g., "IBM").
intervalYesTime interval (e.g., "daily", "60min", "weekly"). Check Alpha Vantage docs for valid intervals per indicator.
datatypeNoData format for the response.json
monthNoSpecific month for intraday intervals (YYYY-MM format).
time_periodYesNumber of data points used to calculate the indicator.
series_typeYesThe desired price type.

Implementation Reference

  • src/index.ts:998-1012 (registration)
    Registers the MCP tool 'technicalIndicators_ema' with server.addTool, including name, description, parameters schema from schemas.TechnicalIndicatorsTimeSeriesIndicatorParamsSchema, and an execute handler that delegates to executeAvantageTool calling the AVantage library's ema method.
    server.addTool({
      name: "technicalIndicators_ema",
      description: "Exponential Moving Average (EMA)",
      parameters: schemas.TechnicalIndicatorsTimeSeriesIndicatorParamsSchema,
      execute: (
        args,
        context // Let type be inferred
      ) =>
        executeAvantageTool(
          "technicalIndicators_ema",
          args,
          context,
          (av, params) => av.technicalIndicators.ema(params)
        ),
    });
  • Defines TechnicalIndicatorsCommonIndicatorParamsSchema (base parameters: symbol, interval, etc.) extended by TechnicalIndicatorsTimeSeriesIndicatorParamsSchema adding time_period and series_type, which validates the input for the technicalIndicators_ema tool.
    export const TechnicalIndicatorsCommonIndicatorParamsSchema = z.object({
      symbol: z.string().describe('The stock symbol (e.g., "IBM").'),
      interval: z.string().describe('Time interval (e.g., "daily", "60min", "weekly"). Check Alpha Vantage docs for valid intervals per indicator.'),
      datatype: DatatypeSchema.default('json').optional(),
      month: z.string().optional().describe('Specific month for intraday intervals (YYYY-MM format).'),
    }).describe('Common parameters for many technical indicators.')
    
    // Schema for indicators requiring time_period and series_type
    export const TechnicalIndicatorsTimeSeriesIndicatorParamsSchema = TechnicalIndicatorsCommonIndicatorParamsSchema.extend({
      time_period: z.string().describe('Number of data points used to calculate the indicator.'), // Using string as AV API expects string
      series_type: SeriesTypeSchema,
    }).describe('Parameters for time series based technical indicators.')
  • Generic executeAvantageTool function that implements the tool execution logic shared by all tools: resolves API key, manages AVantage client via resourceManager, invokes av.technicalIndicators.ema(params) for this tool, stringifies the result, and handles errors.
    async function executeAvantageTool<TArgs, TResult>(
      toolName: string,
      args: TArgs,
      context: Context<Record<string, unknown> | undefined>, // Use the imported Context type directly
      avantageMethod: (
        av: AVantage,
        args: TArgs
      ) => Promise<{ error?: boolean; reason?: string; data?: TResult }>
    ): Promise<string> {
      logger.info(`Executing '${toolName}' tool for request ID: ${context}`);
      logger.debug(`Args for ${toolName}: ${JSON.stringify(args)}`);
    
      // --- Authentication & Resource Management ---
      // Access extraArgs safely - it might be null or undefined
      const extraArgsApiKey = context.extraArgs?.apiKey as string | undefined;
      const apiKey = extraArgsApiKey || config.apiKey;
    
      if (!apiKey) {
        logger.error(`'${toolName}' failed: Alpha Vantage API key missing.`);
        throw new UserError(apiKeyErrorMessage);
      }
      logger.debug(
        `Using AV API key (source: ${extraArgsApiKey ? "extraArgs" : "environment"}) for ${toolName}`
      );
    
      try {
        // Get or create AVantage instance managed by ResourceManager
        const av = await resourceManager.getResource<AVantage>(
          apiKey, // Cache key is the resolved API key
          "avantage_client", // Type identifier for logging
          async (key) => {
            // Factory Function
            logger.info(
              `Creating new AVantage instance for key ending ...${key.slice(-4)}`
            );
            // AVantage library reads AV_PREMIUM from process.env internally
            return new AVantage(key);
          },
          async (avInstance) => {
            // Cleanup Function (no-op needed for AVantage)
            logger.debug(`Destroying AVantage instance (no-op)`);
          }
        );
    
        // --- Library Call ---
        const result = await avantageMethod(av, args);
    
        // --- Response Handling ---
        if (result.error) {
          logger.warn(
            `'${toolName}' failed. Reason from avantage: ${result.reason}`
          );
          throw new UserError(result.reason || `Tool '${toolName}' failed.`);
        }
    
        if (result.data === undefined || result.data === null) {
          logger.warn(`'${toolName}' completed successfully but returned no data.`);
          return "null"; // Return string "null" for empty data
        }
    
        logger.info(`'${toolName}' completed successfully.`);
        // Stringify the data part of the response
        return JSON.stringify(result.data);
      } catch (error: any) {
        logger.error(
          `Error during execution of '${toolName}': ${error.message}`,
          error
        );
        // If it's already a UserError, rethrow it
        if (error instanceof UserError) {
          throw error;
        }
        // Otherwise, wrap it in a UserError
        throw new UserError(
          `An unexpected error occurred while executing tool '${toolName}': ${error.message}`
        );
      }
    }
  • ResourceManager class used by executeAvantageTool to cache and lifecycle-manage AVantage API client instances keyed by API key, ensuring efficient reuse and cleanup.
    export class ResourceManager {
      private resources: Map<string, ResourceInfo<any>> = new Map()
      private readonly cleanupIntervalMs: number
      private cleanupTimer: NodeJS.Timeout | null = null
    
      constructor(options?: { cleanupIntervalMs?: number }) {
        this.cleanupIntervalMs =
          options?.cleanupIntervalMs ?? config.resourceCleanupInterval
        this.startCleanupTimer()
        logger.info(
          `ResourceManager initialized. Cleanup interval: ${this.cleanupIntervalMs}ms`,
        )
      }
    
      /**
       * Retrieves an existing resource or creates a new one.
       */
      public async getResource<T>(
        key: string,
        resourceType: string,
        factoryFn: (key: string) => Promise<T>,
        cleanupFn: (resource: T) => Promise<void>,
      ): Promise<T> {
        const existingInfo = this.resources.get(key)
    
        if (existingInfo) {
          logger.debug(
            `Reusing existing resource (Type: ${existingInfo.resourceType}, Instance ID: ${existingInfo.instanceId}) for key ending with ...${key.slice(-4)}`,
          )
          existingInfo.lastUsed = Date.now()
          if (existingInfo.resourceType !== resourceType) {
            logger.warn(
              `Resource type mismatch for key ${key}. Expected ${resourceType}, found ${existingInfo.resourceType}. Returning existing resource anyway.`,
            )
          }
          return existingInfo.resource as T
        }
    
        logger.info(
          `Creating new resource (Type: ${resourceType}) for key ending with ...${key.slice(-4)}`,
        )
        try {
          const newResource = await factoryFn(key)
          const instanceId = uuidv4()
          const newInfo: ResourceInfo<T> = {
            resource: newResource,
            lastUsed: Date.now(),
            instanceId: instanceId,
            resourceType: resourceType,
            cacheKey: key,
            cleanupFn: cleanupFn,
          }
          this.resources.set(key, newInfo)
          logger.info(
            `Successfully created resource (Type: ${resourceType}, Instance ID: ${instanceId})`,
          )
          return newResource
        } catch (error) {
          logger.error(
            `Failed to create resource (Type: ${resourceType}) for key ${key}: ${error instanceof Error ? error.message : String(error)}`,
            error,
          )
          throw new Error(
            `Resource factory function failed for type ${resourceType}: ${error instanceof Error ? error.message : String(error)}`,
          )
        }
      }
    
      private startCleanupTimer(): void {
        if (this.cleanupTimer) {
          clearInterval(this.cleanupTimer)
        }
        this.cleanupTimer = setInterval(
          () => this.cleanupInactiveResources(),
          this.cleanupIntervalMs,
        )
        this.cleanupTimer.unref()
        logger.info('Resource cleanup timer started.')
      }
    
      public stopCleanupTimer(): void {
        if (this.cleanupTimer) {
          clearInterval(this.cleanupTimer)
          this.cleanupTimer = null
          logger.info('Resource cleanup timer stopped.')
        }
      }
    
      private async cleanupInactiveResources(): Promise<void> {
        const now = Date.now()
        let cleanedCount = 0
        logger.debug('Running inactive resource cleanup check...')
    
        const keysToRemove: string[] = []
        for (const [key, info] of this.resources.entries()) {
          if (now - info.lastUsed > this.cleanupIntervalMs) {
            keysToRemove.push(key)
          }
        }
    
        if (keysToRemove.length === 0) {
          logger.debug('No inactive resources found to clean up.')
          return
        }
    
        logger.info(`Found ${keysToRemove.length} inactive resource(s) to clean up.`)
    
        for (const key of keysToRemove) {
          const info = this.resources.get(key)
          // Double-check inactivity before removing, in case it was accessed again
          if (info && now - info.lastUsed > this.cleanupIntervalMs) {
            logger.info(
              `Cleaning up inactive resource (Type: ${info.resourceType}, Instance ID: ${info.instanceId}, Key: ...${key.slice(-4)})`,
            )
            try {
              await info.cleanupFn(info.resource)
              this.resources.delete(key)
              cleanedCount++
              logger.info(
                `Successfully cleaned up resource (Instance ID: ${info.instanceId})`,
              )
            } catch (error) {
              logger.error(
                `Error during cleanup of resource (Instance ID: ${info.instanceId}): ${error instanceof Error ? error.message : String(error)}`,
                error,
              )
              // Optionally remove from map even if cleanup failed
              // this.resources.delete(key);
            }
          }
        }
    
        if (cleanedCount > 0) {
          logger.info(`Finished cleanup. Removed ${cleanedCount} inactive resource(s).`)
        } else {
          logger.debug('Finished cleanup check, no resources were removed this cycle.')
        }
      }
    
      /**
       * Immediately destroys all managed resources. Useful for graceful shutdown.
       */
      public async destroyAllNow(): Promise<void> {
        logger.warn('Destroying all managed resources immediately...')
        this.stopCleanupTimer()
    
        const cleanupPromises: Promise<void>[] = []
        for (const [key, info] of this.resources.entries()) {
          logger.info(
            `Initiating immediate cleanup for resource (Type: ${info.resourceType}, Instance ID: ${info.instanceId}, Key: ...${key.slice(-4)})`,
          )
          cleanupPromises.push(
            info
              .cleanupFn(info.resource)
              .catch((error) =>
                logger.error(
                  `Error during immediate cleanup of resource (Instance ID: ${info.instanceId}): ${error instanceof Error ? error.message : String(error)}`,
                  error,
                ),
              ),
          )
        }
    
        await Promise.allSettled(cleanupPromises)
    
        const finalCount = this.resources.size
        this.resources.clear()
        logger.warn(
          `Finished destroying all resources. Cleared ${finalCount} resource entries.`,
        )
      }
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure but offers none. It doesn't indicate whether this is a read-only operation, what data source it queries (Alpha Vantage), potential rate limits, authentication requirements, or what the response contains. The description merely names the indicator without explaining its operational behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise (just 3 words) but this brevity comes at the cost of being under-specified rather than efficient. While it's front-loaded with the essential term, it lacks the necessary explanatory content that would make it genuinely helpful for an AI agent.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a technical indicator tool with 6 parameters, no annotations, and no output schema, the description is incomplete. It doesn't explain what the tool returns, how the EMA calculation works, typical use cases in financial analysis, or how this differs from similar indicators. The description fails to compensate for the lack of structured metadata about the tool's behavior and output.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all 6 parameters thoroughly with descriptions and enums. The description adds no parameter information beyond what's in the schema. According to scoring rules, when schema coverage is high (>80%), the baseline is 3 even with no param info in the description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose2/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description 'Exponential Moving Average (EMA)' is a tautology that restates the tool name with minimal expansion. It doesn't specify what the tool actually does (e.g., 'calculates the Exponential Moving Average for a given stock symbol'), nor does it distinguish this tool from sibling technical indicators like 'sma' or 'wma' beyond the different acronym.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines1/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention when EMA is appropriate compared to other technical indicators, what financial analysis contexts it's used for, or any prerequisites for usage. The sibling tool list shows many similar technical indicators, but the description offers no differentiation.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other 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/MissionSquad/mcp-avantage'

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