Lesson

Using Resources

So far, you have provided access to your Neo4j database through tools, which allow the LLM to query the database dynamically based on a fixed set of parameters.

Now you'll learn about resources - a way to expose data through static URIs, similar to a REST API.

What are Resources?

Resources are data that can be loaded into the LLM's context by the client application.

Unlike tools, which the LLM decides to call, resources are application-controlled - the client decides what to load.

Tools vs Resources

Understanding when to use each is key to effective MCP server design:

Use Tools when:

  • The LLM should decide when to access the data
  • You need to perform computation or filtering
  • The operation might have side effects
  • You want the LLM to discover capabilities dynamically

Example: getMoviesByGenre(genre: string) - The LLM decides when to search

Use Resources when:

  • You are exposing static reference data or documentation that has a unique identifier
  • The data should be loaded into context upfront
  • You want to provide specific pieces of data by ID

Example: genre://Comedy - The client directly requests a specific genre

Creating Resources

The TypeScript MCP SDK provides registerResource() to expose resources. Here is a static resource that provides reference data:

typescript
server.registerResource(
  "genres",
  "catalog://genres",
  { description: "All available movie genres with their counts", mimeType: "application/json" },
  async () => {
    const { records } = await driver.executeQuery(
      `
      MATCH (g:Genre)
      RETURN g.name AS name,
             count{ (g)<-[:IN_GENRE]-() } AS movieCount
      ORDER BY g.name
      `,
      {},
      { database }
    );
    return {
      contents: [{
        uri: "catalog://genres",
        mimeType: "application/json",
        text: JSON.stringify({ genres: records.map(r => r.toObject()) }, null, 2),
      }],
    };
  }
);

The registerResource() method takes four arguments:

  1. Name - A human-readable name for the resource ("genres")
  2. URI - The static URI that clients use to request this resource ("catalog://genres")
  3. Options - Metadata including description and mimeType
  4. Handler - An async function that returns the resource content

The catalog://genres URI is static - it does not need any parameters because it always returns the complete list of genres. The handler queries Neo4j for all Genre nodes and returns them as JSON.

Return structured data

Resources should return structured data (JSON) that clients can parse programmatically. This makes it easy for applications to:

  • Process the data consistently
  • Extract specific fields
  • Handle the data in a type-safe way

Resource Templates

While static resources are great for reference data, sometimes you need to access specific entities. The TypeScript SDK provides ResourceTemplate for dynamic URIs with parameters.

typescript
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
 
server.registerResource(
  "movie",
  new ResourceTemplate("movie://{tmdbId}", { list: undefined }),
  { description: "Get details about a specific movie by TMDB ID", mimeType: "application/json" },
  async (uri, { tmdbId }) => {
    const { records } = await driver.executeQuery(
      `
      MATCH (m:Movie {tmdbId: $tmdbId})
      RETURN m.title AS title,
             m.tagline AS tagline,
             m.released AS released,
             m.plot AS plot,
             [ (m)-[:IN_GENRE]->(g:Genre) | g.name ] AS genres
      `,
      { tmdbId: String(tmdbId) },
      { database }
    );
 
    if (records.length === 0) {
      return {
        contents: [{
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify({ error: `Movie ${tmdbId} not found` }),
        }],
      };
    }
 
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify(records[0].toObject(), null, 2),
      }],
    };
  }
);

The URI pattern movie://{tmdbId} introduces a dynamic element. When a client requests movie://603, the SDK extracts "603" and passes it to your handler function's second parameter as { tmdbId: "603" }.

The handler's first parameter, uri, is the parsed URL object. Use uri.href to get the full URI string for the response.

ResourceTemplate options

The second argument to ResourceTemplate is an options object. Setting list: undefined means the server does not provide a way to enumerate all available movies. If you wanted to support listing, you would provide a callback function that returns available resource URIs.

The dynamic pattern allows clients to:

  • Request movie://603 to get The Matrix
  • Request movie://605 to get The Matrix Reloaded

When to Use Resources

Ideal use cases:

  • Reference data - Movie details, person profiles, genre information
  • Documentation - API docs, server capabilities, usage examples
  • Configuration - Server settings, available options
  • Static content - About pages, help text, terms of service
  • Specific entities - Get one item by ID

Not ideal for:

  • Dynamic searches - Use tools instead
  • Filtered lists - Use tools with parameters
  • Computed results - Use tools for computation
  • Operations with side effects - Definitely use tools