Skip to main content
Glama
MoralisWeb3

Moralis MCP Server

Official
by MoralisWeb3

evm_gettokenowners

Identify major holders of an ERC20 token and analyze their ownership percentages, including known entities and exchange wallets.

Instructions

Identify the major holders of an ERC20 token and understand their ownership percentages. Includes known entities, exchanges and wallet labels.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
chainNoThe chain to queryeth
token_addressYesThe address of the token contract
limitNoThe desired page size of the result.
cursorNoThe cursor returned in the previous response (used for getting the next page).
orderNoThe order of the result, in ascending (ASC) or descending (DESC)DESC

Implementation Reference

  • Generic handler function that executes the underlying Moralis API call for the 'evm_gettokenowners' tool (and all other extracted tools). Constructs the HTTP request from the tool definition, validates arguments with Zod, applies security (API key), sends via axios, and formats the response.
    export async function executeApiTool(
      toolName: string,
      definition: McpToolDefinition,
      toolArgs: JsonObject,
      allSecuritySchemes: Record<string, any>,
      token?: string,
    ): Promise<CallToolResult> {
      try {
        // Validate arguments against the input schema
        let validatedArgs: JsonObject;
        try {
          const zodSchema = getZodSchemaFromJsonSchema(
            definition.inputSchema,
            toolName,
          );
          const argsToParse =
            typeof toolArgs === 'object' && toolArgs !== null ? toolArgs : {};
          validatedArgs = zodSchema.parse(argsToParse);
        } catch (error: unknown) {
          if (error instanceof ZodError) {
            const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map((e) => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`;
            return { content: [{ type: 'text', text: validationErrorMessage }] };
          } else {
            const errorMessage =
              error instanceof Error ? error.message : String(error);
            return {
              content: [
                {
                  type: 'text',
                  text: `Internal error during validation setup: ${errorMessage}`,
                },
              ],
            };
          }
        }
    
        // Prepare URL, query parameters, headers, and request body
        let urlPath = definition.pathTemplate;
        const queryParams: Record<string, any> = {};
        const headers: Record<string, string> = {
          Accept: 'application/json',
          'X-Moralis-Platform': 'MCP',
        };
        let requestBodyData: any = undefined;
    
        // Apply parameters to the URL path, query, or headers
        definition.executionParameters.forEach((param) => {
          const value = validatedArgs[param.name];
          if (typeof value !== 'undefined' && value !== null) {
            if (param.in === 'path') {
              urlPath = urlPath.replace(
                `{${param.name}}`,
                encodeURIComponent(String(value)),
              );
            } else if (param.in === 'query') {
              queryParams[param.name] = value;
            } else if (param.in === 'header') {
              headers[param.name.toLowerCase()] = String(value);
            }
          }
        });
    
        // Ensure all path parameters are resolved
        if (urlPath.includes('{')) {
          throw new Error(`Failed to resolve path parameters: ${urlPath}`);
        }
    
        // Construct the full URL
        const requestUrl = `${definition.baseUrl}${urlPath}`;
    
        // Handle request body if needed
        if (
          definition.requestBodyContentType &&
          typeof validatedArgs['requestBody'] !== 'undefined'
        ) {
          requestBodyData = validatedArgs['requestBody'];
          headers['content-type'] = definition.requestBodyContentType;
        }
    
        // Apply security requirements if available
        // Security requirements use OR between array items and AND within each object
        const appliedSecurity = definition.securityRequirements?.find((req) => {
          // Try each security requirement (combined with OR)
          return Object.entries(req).every(([schemeName, scopesArray]) => {
            const scheme = allSecuritySchemes[schemeName];
            if (!scheme) return false;
    
            // API Key security (header, query, cookie)
            if (scheme.type === 'apiKey') {
              return !!token || !!Config.MORALIS_API_KEY;
            }
    
            return false;
          });
        });
    
        // If we found matching security scheme(s), apply them
        if (appliedSecurity) {
          // Apply each security scheme from this requirement (combined with AND)
          for (const [schemeName, scopesArray] of Object.entries(appliedSecurity)) {
            const scheme = allSecuritySchemes[schemeName];
    
            // API Key security
            if (scheme?.type === 'apiKey') {
              const apiKey = token || Config.MORALIS_API_KEY;
              if (apiKey) {
                if (scheme.in === 'header') {
                  headers[scheme.name.toLowerCase()] = apiKey;
                  console.error(
                    `Applied API key '${schemeName}' in header '${scheme.name}'`,
                  );
                }
              }
            }
          }
        }
        // Log warning if security is required but not available
        else if (definition.securityRequirements?.length > 0) {
          // First generate a more readable representation of the security requirements
          const securityRequirementsString = definition.securityRequirements
            .map((req) => {
              const parts = Object.entries(req)
                .map(([name, scopesArray]) => {
                  const scopes = scopesArray as string[];
                  if (scopes.length === 0) return name;
                  return `${name} (scopes: ${scopes.join(', ')})`;
                })
                .join(' AND ');
              return `[${parts}]`;
            })
            .join(' OR ');
    
          console.warn(
            `Tool '${toolName}' requires security: ${securityRequirementsString}, but no suitable credentials found.`,
          );
        }
    
        // Prepare the axios request configuration
        const config: AxiosRequestConfig = {
          method: definition.method.toUpperCase(),
          url: requestUrl,
          params: queryParams,
          headers: headers,
          ...(requestBodyData !== undefined && { data: requestBodyData }),
        };
    
        // Log request info to stderr (doesn't affect MCP output)
        console.error(
          `Executing tool "${toolName}": ${config.method} ${config.url}`,
        );
    
        // Execute the request
        const response = await axios(config);
    
        // Process and format the response
        let responseText = '';
        const contentType = response.headers['content-type']?.toLowerCase() || '';
    
        // Handle JSON responses
        if (
          contentType.includes('application/json') &&
          typeof response.data === 'object' &&
          response.data !== null
        ) {
          try {
            responseText = JSON.stringify(response.data, null, 2);
          } catch (e) {
            responseText = '[Stringify Error]';
          }
        }
        // Handle string responses
        else if (typeof response.data === 'string') {
          responseText = response.data;
        }
        // Handle other response types
        else if (response.data !== undefined && response.data !== null) {
          responseText = String(response.data);
        }
        // Handle empty responses
        else {
          responseText = `(Status: ${response.status} - No body content)`;
        }
    
        // Return formatted response
        return {
          content: [
            {
              type: 'text',
              text: `API Response (Status: ${response.status}):\n${responseText}`,
            },
          ],
        };
      } catch (error: unknown) {
        // Handle errors during execution
        let errorMessage: string;
    
        // Format Axios errors specially
        if (axios.isAxiosError(error)) {
          errorMessage = formatApiError(error);
        }
        // Handle standard errors
        else if (error instanceof Error) {
          errorMessage = error.message;
        }
        // Handle unexpected error types
        else {
          errorMessage = `Unexpected error: ${String(error)}`;
        }
    
        // Log error to stderr
        console.error(
          `Error during execution of tool '${toolName}':`,
          errorMessage,
        );
    
        // Return error message to client
        return { content: [{ type: 'text', text: errorMessage }] };
      }
    }
  • src/server.ts:26-44 (registration)
    Registers tools from EVM OpenAPI spec by extracting operations, applying 'evm_' prefix (so operationId 'gettokenowners' becomes 'evm_gettokenowners'), and building the toolDefinitionMap used for list/call.
    async function mapToolDefinitions(config: SchemaConfig) {
      const spec = (await getSpec(config.specUrl)) as OpenAPIV3DocumentX;
      const api = (await SwaggerParser.dereference(spec)) as OpenAPIV3DocumentX;
    
      const tools = extractToolsFromApi(api, config.prefix);
      const blacklist = Array.isArray(api['x-mcp-blacklist'])
        ? api['x-mcp-blacklist'].map((e) => `${config.prefix}${e.toLowerCase()}`)
        : [];
    
      const toolDefinitionMap: Record<string, McpToolDefinition> = {};
      for (const tool of tools) {
        if (blacklist.includes(tool.name)) continue;
        toolDefinitionMap[tool.name] = {
          ...tool,
          baseUrl: config.baseUrl,
        };
      }
      return toolDefinitionMap;
    }
  • Generates the inputSchema JSON Schema for validation of tool arguments, based on OpenAPI operation parameters (path/query/header) and requestBody.
    export function generateInputSchemaAndDetails(
      operation: OpenAPIV3.OperationObject,
    ): {
      inputSchema: JSONSchema7 | boolean;
      parameters: OpenAPIV3.ParameterObject[];
      requestBodyContentType?: string;
    } {
      const properties: { [key: string]: JSONSchema7 | boolean } = {};
      const required: string[] = [];
    
      // Process parameters
      const allParameters: OpenAPIV3.ParameterObject[] = Array.isArray(
        operation.parameters,
      )
        ? operation.parameters.map((p) => p as OpenAPIV3.ParameterObject)
        : [];
    
      allParameters.forEach((param) => {
        if (!param.name || !param.schema) return;
    
        const paramSchema = mapOpenApiSchemaToJsonSchema(
          param.schema as OpenAPIV3.SchemaObject,
        );
        if (typeof paramSchema === 'object') {
          paramSchema.description = param.description || paramSchema.description;
        }
    
        properties[param.name] = paramSchema;
        if (param.required) required.push(param.name);
      });
    
      // Process request body (if present)
      let requestBodyContentType: string | undefined = undefined;
    
      if (operation.requestBody) {
        const opRequestBody = operation.requestBody as OpenAPIV3.RequestBodyObject;
        const jsonContent = opRequestBody.content?.['application/json'];
        const firstContent = opRequestBody.content
          ? Object.entries(opRequestBody.content)[0]
          : undefined;
    
        if (jsonContent?.schema) {
          requestBodyContentType = 'application/json';
          const bodySchema = mapOpenApiSchemaToJsonSchema(
            jsonContent.schema as OpenAPIV3.SchemaObject,
          );
    
          if (typeof bodySchema === 'object') {
            bodySchema.description =
              opRequestBody.description ||
              bodySchema.description ||
              'The JSON request body.';
          }
    
          properties['requestBody'] = bodySchema;
          if (opRequestBody.required) required.push('requestBody');
        } else if (firstContent) {
          const [contentType] = firstContent;
          requestBodyContentType = contentType;
    
          properties['requestBody'] = {
            type: 'string',
            description:
              opRequestBody.description ||
              `Request body (content type: ${contentType})`,
          };
    
          if (opRequestBody.required) required.push('requestBody');
        }
      }
    
      // Combine everything into a JSON Schema
      const inputSchema: JSONSchema7 = {
        type: 'object',
        properties,
        ...(required.length > 0 && { required }),
      };
    
      return { inputSchema, parameters: allParameters, requestBodyContentType };
    }
  • Core helper that iterates OpenAPI paths/operations, sanitizes operationId to create tool name (e.g. 'getTokenOwners' -> 'gettokenowners'), adds prefix 'evm_', generates schema, for 'evm_gettokenowners'.
    export function extractToolsFromApi(
      api: OpenAPIV3DocumentX,
      prefix?: string
    ): McpToolDefinition[] {
      const tools: McpToolDefinition[] = [];
      const usedNames = new Set<string>();
      const globalSecurity = api.security || [];
    
      if (!api.paths) return tools;
    
      for (const [path, pathItem] of Object.entries(api.paths)) {
        if (!pathItem) continue;
    
        for (const method of Object.values(OpenAPIV3.HttpMethods)) {
          const operation = pathItem[method];
          if (!operation) continue;
    
          // Generate a unique name for the tool
          let baseName = operation.operationId || generateOperationId(method, path);
          if (!baseName) continue;
    
          // Sanitize the name to be MCP-compatible (only a-z, 0-9, _, -)
          baseName = baseName
            .replace(/\./g, '_')
            .replace(/[^a-z0-9_-]/gi, '_')
            .toLowerCase();
    
          let finalToolName = baseName;
          let counter = 1;
          while (usedNames.has(finalToolName)) {
            finalToolName = `${baseName}_${counter++}`;
          }
          usedNames.add(finalToolName);
    
          // Get or create a description
          const description =
            operation.description ||
            operation.summary ||
            `Executes ${method.toUpperCase()} ${path}`;
    
          const prompt = operation['x-mcp-prompt'];
    
          // Generate input schema and extract parameters
          const { inputSchema, parameters, requestBodyContentType } =
            generateInputSchemaAndDetails(operation);
    
          // Extract parameter details for execution
          const executionParameters = parameters.map((p) => ({
            name: p.name,
            in: p.in,
          }));
    
          // Determine security requirements
          const securityRequirements =
            operation.security === null
              ? globalSecurity
              : operation.security || globalSecurity;
    
          // Create the tool definition
          tools.push({
            name: prefix + finalToolName,
            description,
            inputSchema,
            method,
            pathTemplate: path,
            parameters,
            executionParameters,
            requestBodyContentType,
            securityRequirements,
            operationId: baseName,
            prompt,
          });
        }
      }
    
      return tools;
    }
  • src/config.ts:15-22 (registration)
    EVM configuration defining the 'evm_' prefix and URLs for the Moralis Deep Index API spec from which 'evm_gettokenowners' is extracted.
    evm:  {
      prefix: 'evm_',
      baseUrl:
        process.env.API_BASE_URL || 'https://deep-index.moralis.io/api/v2.2',
      specUrl:
        process.env.API_SPEC_URL ||
        'https://deep-index.moralis.io/api-docs-2.2/v2.2/swagger.json',
    },
Behavior2/5

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

No annotations are provided, so the description carries the full burden. It mentions that the tool 'Includes known entities, exchanges and wallet labels,' which adds some behavioral context about enriched data. However, it lacks critical details: it doesn't specify if this is a read-only operation, whether it's paginated (implied by 'limit' and 'cursor' parameters but not stated), rate limits, or authentication needs. The description is insufficient for a tool with 5 parameters and no annotation coverage.

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

Conciseness4/5

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

The description is concise and front-loaded with the core purpose in the first sentence. The second sentence adds useful context about included data. There's no wasted verbiage, and it's appropriately sized for the tool's complexity.

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?

Given the complexity (5 parameters, no annotations, no output schema), the description is incomplete. It lacks behavioral transparency, usage guidelines, and details on output format (e.g., what 'ownership percentages' mean or how results are structured). For a tool that likely returns paginated data with enriched holder information, more context is needed to be fully helpful.

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?

The schema description coverage is 100%, so the schema already documents all parameters thoroughly. The description doesn't add any specific meaning beyond what the schema provides (e.g., it doesn't explain how 'token_address' relates to ERC20 tokens or clarify 'major holders' criteria). With high schema coverage, the baseline is 3, and the description doesn't compensate with additional insights.

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

Purpose4/5

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

The description clearly states the tool's purpose: 'Identify the major holders of an ERC20 token and understand their ownership percentages.' It specifies the resource (ERC20 token holders) and the action (identify major holders with percentages). However, it doesn't explicitly differentiate from sibling tools like 'evm_gettokenholders' or 'evm_gethistoricaltokenholders,' which appear to be related but not clearly distinguished.

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

Usage Guidelines2/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 mentions 'Includes known entities, exchanges and wallet labels,' which hints at enriched data, but doesn't specify if this is for current vs. historical holders or how it differs from siblings like 'evm_gettokenholders.' No explicit when/when-not or alternative recommendations are provided.

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/MoralisWeb3/moralis-mcp-server'

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