Skip to main content
Glama

getNFTMetadata

Retrieve ERC-721 NFT metadata including name, description, image, and attributes from EVM chains. Supports automatic IPFS URI conversion for accessible data retrieval.

Instructions

ERC-721 NFT의 메타데이터(이름, 설명, 이미지, 속성)를 조회합니다. IPFS URI 자동 변환 지원

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
contractAddressYesNFT 컨트랙트 주소 (0x...)
tokenIdYes조회할 토큰 ID
chainNoEVM 체인ethereum

Implementation Reference

  • The 'handler' function executes the core logic of 'getNFTMetadata', including fetching the token URI, handling IPFS/base64 data, fetching metadata via HTTP, and caching the result.
    async function handler(args: z.infer<typeof inputSchema>): Promise<ToolResult<NFTMetadataData>> {
      const { contractAddress, tokenId, chain } = args;
    
      const cacheKey = `nftmeta:${chain}:${contractAddress.toLowerCase()}:${tokenId}`;
      const cached = cache.get<NFTMetadataData>(cacheKey);
      if (cached.hit) return makeSuccess(chain, cached.data, true);
    
      // tokenId를 bigint로 변환 (유효성 검사)
      let tokenIdBigInt: bigint;
      try {
        tokenIdBigInt = BigInt(tokenId);
      } catch {
        return makeError(`Invalid tokenId: '${tokenId}' is not a valid integer`, "INVALID_INPUT");
      }
    
      try {
        const client = getClient(chain);
    
        // 1단계: 컨트랙트에서 tokenURI 조회
        const rawURI = await client.readContract({
          address: contractAddress as `0x${string}`,
          abi: ERC721_TOKEN_URI_ABI,
          functionName: "tokenURI",
          args: [tokenIdBigInt],
        }) as string;
    
        // IPFS URI 처리
        const resolvedURI = resolveURI(rawURI);
    
        // data URI (base64 인코딩된 JSON) 처리
        if (resolvedURI.startsWith("data:application/json")) {
          try {
            const base64Part = resolvedURI.split(",")[1];
            const jsonStr = Buffer.from(base64Part, "base64").toString("utf-8");
            const meta = JSON.parse(jsonStr);
    
            const data: NFTMetadataData = {
              contractAddress,
              tokenId,
              tokenURI: rawURI,
              name: meta.name,
              description: meta.description,
              image: meta.image ? resolveURI(meta.image) : undefined,
              attributes: meta.attributes,
            };
    
            cache.set(cacheKey, data, NFT_METADATA_CACHE_TTL);
            return makeSuccess(chain, data, false);
          } catch {
            return makeError(`Failed to parse base64-encoded metadata for tokenId ${tokenId}`, "RPC_ERROR");
          }
        }
    
        // 2단계: SSRF 방지 — 내부 네트워크 차단
        if (!isAllowedURL(resolvedURI)) {
          return makeError("Metadata URI points to a blocked or invalid address", "INVALID_INPUT");
        }
    
        // URI로 HTTP 요청하여 메타데이터 JSON 취득
        let metaJson: Record<string, unknown>;
        try {
          const response = await fetch(resolvedURI, {
            headers: { Accept: "application/json" },
            signal: AbortSignal.timeout(10000), // 10초 타임아웃
          });
    
          if (!response.ok) {
            return makeError(
              `Failed to fetch metadata from URI: HTTP ${response.status}`,
              "API_ERROR",
            );
          }
    
          const contentType = response.headers.get("content-type") ?? "";
          if (!contentType.includes("json") && !contentType.includes("text")) {
            return makeError(
              `Unexpected content-type '${contentType}' from metadata URI`,
              "API_ERROR",
            );
          }
    
          const text = await response.text();
          try {
            metaJson = JSON.parse(text);
          } catch {
            return makeError(`Metadata URI did not return valid JSON for tokenId ${tokenId}`, "API_ERROR");
          }
        } catch (err) {
          // fetch 자체 실패 (네트워크 오류, 타임아웃 등)
          if (err instanceof Error && (err.name === "AbortError" || err.message.includes("timeout"))) {
            return makeError(`Metadata fetch timed out for tokenId ${tokenId}`, "API_ERROR");
          }
          return makeError(`Failed to fetch metadata: ${sanitizeError(err)}`, "API_ERROR");
        }
    
        // 3단계: 메타데이터 필드 추출 및 image URI 정규화
        const data: NFTMetadataData = {
          contractAddress,
          tokenId,
          tokenURI: rawURI,
          name: typeof metaJson.name === "string" ? metaJson.name : undefined,
          description: typeof metaJson.description === "string" ? metaJson.description : undefined,
          image: typeof metaJson.image === "string" ? resolveURI(metaJson.image) : undefined,
          attributes: Array.isArray(metaJson.attributes)
            ? (metaJson.attributes as NFTAttribute[])
            : undefined,
        };
    
        cache.set(cacheKey, data, NFT_METADATA_CACHE_TTL);
        return makeSuccess(chain, data, false);
      } catch (err) {
        return makeError(`Failed to fetch NFT metadata: ${sanitizeError(err)}`, "RPC_ERROR");
      }
    }
  • Input schema definition for the 'getNFTMetadata' tool using Zod.
    const inputSchema = z.object({
      contractAddress: z.string().describe("NFT 컨트랙트 주소 (0x...)"),
      tokenId: z.string().describe("조회할 토큰 ID"),
      chain: z.enum(SUPPORTED_CHAINS).default("ethereum").describe("EVM 체인"),
    });
  • Registration function for the 'getNFTMetadata' tool, exposing the tool to the MCP server.
    export function register(server: McpServer) {
      server.tool(
        "getNFTMetadata",
        "ERC-721 NFT의 메타데이터(이름, 설명, 이미지, 속성)를 조회합니다. IPFS URI 자동 변환 지원",
        inputSchema.shape,
        async (args) => {
          const result = await handler(args as z.infer<typeof inputSchema>);
          return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
        },
      );
    }

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/calintzy/evmscope'

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