Implementing MCP Resources
Workflow
Task Progress:
- [ ] Step 1: Create resource file in src/mcp/resources/
- [ ] Step 2: Define name, URI/template, config, and readCallback
- [ ] Step 3: Export as default ResourcePlugin or ResourceTemplatePlugin
- [ ] Step 4: Register in src/mcp/resources/mod.ts
- [ ] Step 5: (If KV-backed) Add URI-to-key mapping in kvKeys.ts
- [ ] Step 6: Run `deno task ci` to verify
Resource Types
| Type | Use Case | URI | Plugin Type |
|---|---|---|---|
| Static resource | Fixed content at a known URI | "hello://world" |
ResourcePlugin |
| KV-backed resource | Persistent, mutable state | "counter://value" |
ResourcePlugin |
| Resource template | Dynamic URI with variables | "greetings://{name}" |
ResourceTemplatePlugin |
Static Resource Template
Create a new file in src/mcp/resources/.
import type { ResourceMetadata } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import type { ResourcePlugin } from "$/shared/types.ts";
const name = "myResource";
const uri = "my-scheme://my-path";
const config: ResourceMetadata = {
description: "What this resource provides",
mimeType: "text/plain", // or "application/json"
};
async function readCallback(): Promise<ReadResourceResult> {
return {
contents: [{
uri,
text: "Resource content here",
}],
};
}
const module: ResourcePlugin = {
type: "resource",
name,
uri,
config,
readCallback,
};
export default module;
Resource Template (Dynamic URI)
For resources with variable URI segments like greetings://{name}:
import {
type CompleteResourceTemplateCallback,
type ResourceMetadata,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import type { ResourceTemplatePlugin } from "$/shared/types.ts";
const name = "myTemplate";
const completeName: CompleteResourceTemplateCallback = (value) => {
const prefix = value.trim().toLowerCase();
return suggestions.filter((s) => s.toLowerCase().startsWith(prefix)).slice(0, 5);
};
const template = new ResourceTemplate(
"my-scheme://{variable}",
{
list: undefined, // optional: callback to list all instances
complete: { variable: completeName }, // optional: autocomplete per variable
},
);
const config: ResourceMetadata = {
mimeType: "text/plain",
};
async function readCallback(
uri: URL,
variables: Record<string, unknown>,
): Promise<ReadResourceResult> {
const variable = variables.variable as string;
return {
contents: [{
uri: uri.toString(),
text: `Content for ${variable}`,
}],
};
}
const module: ResourceTemplatePlugin = {
type: "template",
name,
template,
config,
readCallback,
};
export default module;
Registration
In src/mcp/resources/mod.ts:
- Import the resource:
import myResource from "./myResource.ts"; - Add it to the
resourcesarray:
export const resources: AnyResourcePlugin[] = [
// ... existing resources
myResource,
];
The server dispatches based on resource.type:
"resource"callsregisterResource(name, uri, config, readCallback)"template"callsregisterResource(name, template, config, readCallback)
Key Types
From src/shared/types.ts:
export type ResourcePlugin = {
type: "resource";
name: string;
uri: string;
config: ResourceMetadata;
readCallback: ReadResourceCallback;
};
export type ResourceTemplatePlugin = {
type: "template";
name: string;
template: ResourceTemplate;
config: ResourceMetadata;
readCallback: ReadResourceTemplateCallback;
};
export type AnyResourcePlugin = ResourcePlugin | ResourceTemplatePlugin;
KV-Backed Resources
For resources with persistent mutable state:
- Create a store file (e.g.
myStore.ts) with KV read/write functions:
import { getKvStore } from "$/kv/mod.ts";
export const MY_KEY: Deno.KvKey = ["resource", "my-resource", "value"];
export async function getValue(): Promise<string> {
const kv = await getKvStore();
const entry = await kv.get<string>(MY_KEY);
return entry.value ?? "default";
}
export async function setValue(value: string): Promise<void> {
const kv = await getKvStore();
await kv.set(MY_KEY, value);
}
- Use in the resource readCallback:
import { getValue } from "./myStore.ts";
async function readCallback(): Promise<ReadResourceResult> {
const value = await getValue();
return { contents: [{ uri, text: JSON.stringify({ value }) }] };
}
- Register the KV key mapping in
src/mcp/resources/kvKeys.ts:
import { MY_URI } from "./myResource.ts";
import { MY_KEY } from "./myStore.ts";
export const RESOURCE_KV_KEYS: ReadonlyMap<string, Deno.KvKey> = new Map([
[COUNTER_URI, COUNTER_KEY],
[MY_URI, MY_KEY], // add your mapping
]);
This enables automatic subscription notifications — when a tool mutates the KV key, the subscription tracker detects the change and notifies subscribed clients.
Subscription Flow
When resourceSubscribe: true in mcpServerDefinition (src/mcp/serverDefinition.ts), resources get subscription support:
- Client subscribes to a URI
subscriptionTracker.subscribe()starts watching the mapped KV key- When the KV key changes, all subscribed clients receive
resourceUpdatednotifications - No manual notification code needed in tools — just mutate KV state
The subscription tracker (src/mcp/resources/subscriptionTracker.ts) handles ref-counting, KV watching, and notifier lifecycle automatically.
ReadResourceResult Format
Both resource types return the same shape:
{
contents: [{
uri: string, // the resource URI
text: string, // text content (for text/plain or application/json)
// OR
blob: string, // base64-encoded binary content
}],
}
Use text for text/JSON, blob for binary data. Set mimeType in config accordingly.
Additional Resources
- For complete resource examples, see examples.md
- MCP Resources spec: https://modelcontextprotocol.io/specification/2025-06-18/server/resources
Source: phughesmcr/deno-mcp-template — distributed by TomeVault.