Skip to main content
Glama
MoralisWeb3

Moralis MCP Server

Official
by MoralisWeb3

evm_getmultiplenfts

Retrieve comprehensive NFT metadata for multiple tokens simultaneously, including on-chain and off-chain data, floor prices, and rarity information.

Instructions

Get NFT metadata for one or many NFTs. Accepts an array of up to 25 tokens, each requiring token_address and token_id. Each NFT returned includes on-chain metadata as well as off-chain metadata, floor prices, rarity and more where available.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
chainNoThe chain to queryeth
requestBodyYesBody

Implementation Reference

  • The primary handler function that implements execution for 'evm_getmultiplenfts' and all other dynamically extracted tools. Validates input arguments using Zod schema derived from OpenAPI, constructs the full API request (path substitution, query params, headers, auth with MORALIS_API_KEY, request body), performs the HTTP request using Axios to the Moralis Deep Index API, processes the response (JSON formatting, status), and returns as MCP CallToolResult.
    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 }] };
      }
    }
  • The MCP server request handler for CallToolRequestSchema. Retrieves the tool definition from the map (including for 'evm_getmultiplenfts'), validates existence, and invokes executeApiTool with arguments, security schemes, and auth token.
    server.setRequestHandler(
      CallToolRequestSchema,
      async (request: CallToolRequest, c): Promise<CallToolResult> => {
        const { name: toolName, arguments: toolArgs } = request.params;
        const toolDefinition = toolDefinitionMap[toolName];
        if (!toolDefinition) {
          console.error(`Error: Unknown tool requested: ${toolName}`);
          return {
            content: [
              {
                type: 'text',
                text: `Error: Unknown tool requested: ${toolName}`,
              },
            ],
          };
        }
        return executeApiTool(
          toolName,
          toolDefinition,
          toolArgs ?? {},
          securitySchemes,
          c.authInfo?.token,
        );
      },
    );
  • src/server.ts:26-44 (registration)
    Loads and parses the Moralis EVM OpenAPI spec, extracts all tools using extractToolsFromApi with 'evm_' prefix (creating 'evm_getmultiplenfts' from the corresponding operationId), applies blacklist filter, and builds toolDefinitionMap for registration.
    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;
    }
  • Core function extractToolsFromApi that iterates OpenAPI paths and operations, generates/sanitizes tool names (e.g., operationId 'getMultipleNFTs' -> 'getmultiplenfts' -> 'evm_getmultiplenfts'), extracts description, security, generates input schema, and creates McpToolDefinition objects.
    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;
    }
  • Generates the input JSON Schema for the tool from OpenAPI operation.parameters and requestBody, mapping schemas, handling content types, marking required fields - used to define inputSchema for 'evm_getmultiplenfts'.
    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 };
    }
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. It mentions the tool returns 'on-chain metadata as well as off-chain metadata, floor prices, rarity and more where available,' which adds some context about return content. However, it lacks critical behavioral details such as rate limits, error handling, authentication requirements, or performance characteristics (e.g., response time, pagination). For a read operation with multiple parameters, this is a significant gap.

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 appropriately sized and front-loaded, with the core purpose stated first. Both sentences add value: the first defines the tool's function and input constraints, the second details the return content. There is no wasted text, making it efficient while covering essential aspects.

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 tool's complexity (handling multiple NFTs with various metadata types), lack of annotations, and no output schema, the description is incomplete. It doesn't explain the structure of returned data, error conditions, or limitations beyond the 25-token cap. For a tool with rich potential outputs and no structured output documentation, more context is needed to guide effective use.

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 parameters ('chain', 'requestBody' with 'tokens', 'normalizeMetadata', 'media_items'). The description adds minimal value beyond the schema by mentioning 'array of up to 25 tokens' and the required fields, but doesn't explain parameter interactions or provide additional semantics. This meets the baseline for high schema coverage.

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: 'Get NFT metadata for one or many NFTs.' It specifies the verb ('Get') and resource ('NFT metadata'), and distinguishes it from siblings like 'evm_getnftmetadata' by handling multiple NFTs. However, it doesn't explicitly contrast with 'evm_getwalletnfts' or 'evm_getnftcollectionstats', leaving some ambiguity about sibling differentiation.

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 the tool accepts an array of tokens but doesn't compare it to similar tools like 'evm_getnftmetadata' (single NFT) or 'evm_getwalletnfts' (NFTs by wallet). There are no explicit when/when-not instructions or named alternatives, leaving usage context implied at best.

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