Repository: modelcontextprotocol/typescript-sdk

Issue:
requestInit.headers do not work
**Describe the bug**
requestInit.headers do not work because requestInit.headers cannot be spread (...) when its type is Headers.
code like below may be fixed.
```
return new Headers(
      { ...headers, ...this._requestInit?.headers }
    );
```

**To Reproduce**
Steps to reproduce the behavior:
```
const headers = new Headers({ "Content-Type": "application/json" });
console.log({ ...headers });
// output is {}
```



Discussion:

Solution patch:
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 0000000..e69de29
diff --git a/README.md b/README.md
index c9e27c2..2800cfb 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@
   - [Resources](#resources)
   - [Tools](#tools)
   - [Prompts](#prompts)
+  - [Completions](#completions)
 - [Running Your Server](#running-your-server)
   - [stdio](#stdio)
   - [Streamable HTTP](#streamable-http)
@@ -54,22 +55,30 @@ import { z } from "zod";
 
 // Create an MCP server
 const server = new McpServer({
-  name: "Demo",
+  name: "demo-server",
   version: "1.0.0"
 });
 
 // Add an addition tool
-server.tool("add",
-  { a: z.number(), b: z.number() },
+server.registerTool("add",
+  {
+    title: "Addition Tool",
+    description: "Add two numbers",
+    inputSchema: { a: z.number(), b: z.number() }
+  },
   async ({ a, b }) => ({
     content: [{ type: "text", text: String(a + b) }]
   })
 );
 
 // Add a dynamic greeting resource
-server.resource(
+server.registerResource(
   "greeting",
   new ResourceTemplate("greeting://{name}", { list: undefined }),
+  { 
+    title: "Greeting Resource",      // Display name for UI
+    description: "Dynamic greeting generator"
+  },
   async (uri, { name }) => ({
     contents: [{
       uri: uri.href,
@@ -100,7 +109,7 @@ The McpServer is your core interface to the MCP protocol. It handles connection
 
 ```typescript
 const server = new McpServer({
-  name: "My App",
+  name: "my-app",
   version: "1.0.0"
 });
 ```
@@ -111,9 +120,14 @@ Resources are how you expose data to LLMs. They're similar to GET endpoints in a
 
 ```typescript
 // Static resource
-server.resource(
+server.registerResource(
   "config",
   "config://app",
+  {
+    title: "Application Config",
+    description: "Application configuration data",
+    mimeType: "text/plain"
+  },
   async (uri) => ({
     contents: [{
       uri: uri.href,
@@ -123,9 +137,13 @@ server.resource(
 );
 
 // Dynamic resource with parameters
-server.resource(
+server.registerResource(
   "user-profile",
   new ResourceTemplate("users://{userId}/profile", { list: undefined }),
+  {
+    title: "User Profile",
+    description: "User profile information"
+  },
   async (uri, { userId }) => ({
     contents: [{
       uri: uri.href,
@@ -133,6 +151,33 @@ server.resource(
     }]
   })
 );
+
+// Resource with context-aware completion
+server.registerResource(
+  "repository",
+  new ResourceTemplate("github://repos/{owner}/{repo}", {
+    list: undefined,
+    complete: {
+      // Provide intelligent completions based on previously resolved parameters
+      repo: (value, context) => {
+        if (context?.arguments?.["owner"] === "org1") {
+          return ["project1", "project2", "project3"].filter(r => r.startsWith(value));
+        }
+        return ["default-repo"].filter(r => r.startsWith(value));
+      }
+    }
+  }),
+  {
+    title: "GitHub Repository",
+    description: "Repository information"
+  },
+  async (uri, { owner, repo }) => ({
+    contents: [{
+      uri: uri.href,
+      text: `Repository: ${owner}/${repo}`
+    }]
+  })
+);
 ```
 
 ### Tools
@@ -141,11 +186,15 @@ Tools let LLMs take actions through your server. Unlike resources, tools are exp
 
 ```typescript
 // Simple tool with parameters
-server.tool(
+server.registerTool(
   "calculate-bmi",
   {
-    weightKg: z.number(),
-    heightM: z.number()
+    title: "BMI Calculator",
+    description: "Calculate Body Mass Index",
+    inputSchema: {
+      weightKg: z.number(),
+      heightM: z.number()
+    }
   },
   async ({ weightKg, heightM }) => ({
     content: [{
@@ -156,9 +205,13 @@ server.tool(
 );
 
 // Async tool with external API call
-server.tool(
+server.registerTool(
   "fetch-weather",
-  { city: z.string() },
+  {
+    title: "Weather Fetcher",
+    description: "Get weather data for a city",
+    inputSchema: { city: z.string() }
+  },
   async ({ city }) => {
     const response = await fetch(`https://api.weather.com/${city}`);
     const data = await response.text();
@@ -167,16 +220,56 @@ server.tool(
     };
   }
 );
+
+// Tool that returns ResourceLinks
+server.registerTool(
+  "list-files",
+  {
+    title: "List Files",
+    description: "List project files",
+    inputSchema: { pattern: z.string() }
+  },
+  async ({ pattern }) => ({
+    content: [
+      { type: "text", text: `Found files matching "${pattern}":` },
+      // ResourceLinks let tools return references without file content
+      {
+        type: "resource_link",
+        uri: "file:///project/README.md",
+        name: "README.md",
+        mimeType: "text/markdown",
+        description: 'A README file'
+      },
+      {
+        type: "resource_link",
+        uri: "file:///project/src/index.ts",
+        name: "index.ts",
+        mimeType: "text/typescript",
+        description: 'An index file'
+      }
+    ]
+  })
+);
 ```
 
+#### ResourceLinks
+
+Tools can return `ResourceLink` objects to reference resources without embedding their full content. This is essential for performance when dealing with large files or many resources - clients can then selectively read only the resources they need using the provided URIs.
+
 ### Prompts
 
 Prompts are reusable templates that help LLMs interact with your server effectively:
 
 ```typescript
-server.prompt(
+import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
+
+server.registerPrompt(
   "review-code",
-  { code: z.string() },
+  {
+    title: "Code Review",
+    description: "Review code for best practices and potential issues",
+    argsSchema: { code: z.string() }
+  },
   ({ code }) => ({
     messages: [{
       role: "user",
@@ -187,6 +280,106 @@ server.prompt(
     }]
   })
 );
+
+// Prompt with context-aware completion
+server.registerPrompt(
+  "team-greeting",
+  {
+    title: "Team Greeting",
+    description: "Generate a greeting for team members",
+    argsSchema: {
+      department: completable(z.string(), (value) => {
+        // Department suggestions
+        return ["engineering", "sales", "marketing", "support"].filter(d => d.startsWith(value));
+      }),
+      name: completable(z.string(), (value, context) => {
+        // Name suggestions based on selected department
+        const department = context?.arguments?.["department"];
+        if (department === "engineering") {
+          return ["Alice", "Bob", "Charlie"].filter(n => n.startsWith(value));
+        } else if (department === "sales") {
+          return ["David", "Eve", "Frank"].filter(n => n.startsWith(value));
+        } else if (department === "marketing") {
+          return ["Grace", "Henry", "Iris"].filter(n => n.startsWith(value));
+        }
+        return ["Guest"].filter(n => n.startsWith(value));
+      })
+    }
+  },
+  ({ department, name }) => ({
+    messages: [{
+      role: "assistant",
+      content: {
+        type: "text",
+        text: `Hello ${name}, welcome to the ${department} team!`
+      }
+    }]
+  })
+);
+```
+
+### Completions
+
+MCP supports argument completions to help users fill in prompt arguments and resource template parameters. See the examples above for [resource completions](#resources) and [prompt completions](#prompts).
+
+#### Client Usage
+
+```typescript
+// Request completions for any argument
+const result = await client.complete({
+  ref: {
+    type: "ref/prompt",  // or "ref/resource"
+    name: "example"      // or uri: "template://..."
+  },
+  argument: {
+    name: "argumentName",
+    value: "partial"     // What the user has typed so far
+  },
+  context: {             // Optional: Include previously resolved arguments
+    arguments: {
+      previousArg: "value"
+    }
+  }
+});
+
+```
+
+### Display Names and Metadata
+
+All resources, tools, and prompts support an optional `title` field for better UI presentation. The `title` is used as a display name, while `name` remains the unique identifier.
+
+**Note:** The `register*` methods (`registerTool`, `registerPrompt`, `registerResource`) are the recommended approach for new code. The older methods (`tool`, `prompt`, `resource`) remain available for backwards compatibility.
+
+#### Title Precedence for Tools
+
+For tools specifically, there are two ways to specify a title:
+- `title` field in the tool configuration
+- `annotations.title` field (when using the older `tool()` method with annotations)
+
+The precedence order is: `title` → `annotations.title` → `name`
+
+```typescript
+// Using registerTool (recommended)
+server.registerTool("my_tool", {
+  title: "My Tool",              // This title takes precedence
+  annotations: {
+    title: "Annotation Title"    // This is ignored if title is set
+  }
+}, handler);
+
+// Using tool with annotations (older API)
+server.tool("my_tool", "description", {
+  title: "Annotation Title"      // This is used as title
+}, handler);
+```
+
+When building clients, use the provided utility to get the appropriate display name:
+
+```typescript
+import { getDisplayName } from "@modelcontextprotocol/sdk/shared/metadataUtils.js";
+
+// Automatically handles the precedence: title → annotations.title → name
+const displayName = getDisplayName(tool);
 ```
 
 ## Running Your Server
@@ -251,7 +444,11 @@ app.post('/mcp', async (req, res) => {
       onsessioninitialized: (sessionId) => {
         // Store the transport by session ID
         transports[sessionId] = transport;
-      }
+      },
+      // DNS rebinding protection is disabled by default for backwards compatibility. If you are running this server
+      // locally, make sure to set:
+      // enableDnsRebindingProtection: true,
+      // allowedHosts: ['127.0.0.1'],
     });
 
     // Clean up transport when closed
@@ -307,6 +504,21 @@ app.delete('/mcp', handleSessionRequest);
 app.listen(3000);
 ```
 
+> [!TIP]
+> When using this in a remote environment, make sure to allow the header parameter `mcp-session-id` in CORS. Otherwise, it may result in a `Bad Request: No valid session ID provided` error. 
+> 
+> For example, in Node.js you can configure it like this:
+> 
+> ```ts
+> app.use(
+>   cors({
+>     origin: ['https://your-remote-domain.com, https://your-other-remote-domain.com'],
+>     exposedHeaders: ['mcp-session-id'],
+>     allowedHeaders: ['Content-Type', 'mcp-session-id'],
+>   })
+> );
+> ```
+
 #### Without Session Management (Stateless)
 
 For simpler use cases where session management isn't needed:
@@ -347,6 +559,7 @@ app.post('/mcp', async (req: Request, res: Response) => {
   }
 });
 
+// SSE notifications not supported in stateless mode
 app.get('/mcp', async (req: Request, res: Response) => {
   console.log('Received GET MCP request');
   res.writeHead(405).end(JSON.stringify({
@@ -359,6 +572,7 @@ app.get('/mcp', async (req: Request, res: Response) => {
   }));
 });
 
+// Session termination not needed in stateless mode
 app.delete('/mcp', async (req: Request, res: Response) => {
   console.log('Received DELETE MCP request');
   res.writeHead(405).end(JSON.stringify({
@@ -386,6 +600,22 @@ This stateless approach is useful for:
 - RESTful scenarios where each request is independent
 - Horizontally scaled deployments without shared session state
 
+#### DNS Rebinding Protection
+
+The Streamable HTTP transport includes DNS rebinding protection to prevent security vulnerabilities. By default, this protection is **disabled** for backwards compatibility.
+
+**Important**: If you are running this server locally, enable DNS rebinding protection:
+
+```typescript
+const transport = new StreamableHTTPServerTransport({
+  sessionIdGenerator: () => randomUUID(),
+  enableDnsRebindingProtection: true,
+
+  allowedHosts: ['127.0.0.1', ...],
+  allowedOrigins: ['https://yourdomain.com', 'https://www.yourdomain.com']
+});
+```
+
 ### Testing and Debugging
 
 To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information.
@@ -401,13 +631,17 @@ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mc
 import { z } from "zod";
 
 const server = new McpServer({
-  name: "Echo",
+  name: "echo-server",
   version: "1.0.0"
 });
 
-server.resource(
+server.registerResource(
   "echo",
   new ResourceTemplate("echo://{message}", { list: undefined }),
+  {
+    title: "Echo Resource",
+    description: "Echoes back messages as resources"
+  },
   async (uri, { message }) => ({
     contents: [{
       uri: uri.href,
@@ -416,17 +650,25 @@ server.resource(
   })
 );
 
-server.tool(
+server.registerTool(
   "echo",
-  { message: z.string() },
+  {
+    title: "Echo Tool",
+    description: "Echoes back the provided message",
+    inputSchema: { message: z.string() }
+  },
   async ({ message }) => ({
     content: [{ type: "text", text: `Tool echo: ${message}` }]
   })
 );
 
-server.prompt(
+server.registerPrompt(
   "echo",
-  { message: z.string() },
+  {
+    title: "Echo Prompt",
+    description: "Creates a prompt to process a message",
+    argsSchema: { message: z.string() }
+  },
   ({ message }) => ({
     messages: [{
       role: "user",
@@ -450,7 +692,7 @@ import { promisify } from "util";
 import { z } from "zod";
 
 const server = new McpServer({
-  name: "SQLite Explorer",
+  name: "sqlite-explorer",
   version: "1.0.0"
 });
 
@@ -463,9 +705,14 @@ const getDb = () => {
   };
 };
 
-server.resource(
+server.registerResource(
   "schema",
   "schema://main",
+  {
+    title: "Database Schema",
+    description: "SQLite database schema",
+    mimeType: "text/plain"
+  },
   async (uri) => {
     const db = getDb();
     try {
@@ -484,9 +731,13 @@ server.resource(
   }
 );
 
-server.tool(
+server.registerTool(
   "query",
-  { sql: z.string() },
+  {
+    title: "SQL Query",
+    description: "Execute SQL queries on the database",
+    inputSchema: { sql: z.string() }
+  },
   async ({ sql }) => {
     const db = getDb();
     try {
@@ -635,6 +886,109 @@ const transport = new StdioServerTransport();
 await server.connect(transport);
 ```
 
+### Eliciting User Input
+
+MCP servers can request additional information from users through the elicitation feature. This is useful for interactive workflows where the server needs user input or confirmation:
+
+```typescript
+// Server-side: Restaurant booking tool that asks for alternatives
+server.tool(
+  "book-restaurant",
+  { 
+    restaurant: z.string(),
+    date: z.string(),
+    partySize: z.number()
+  },
+  async ({ restaurant, date, partySize }) => {
+    // Check availability
+    const available = await checkAvailability(restaurant, date, partySize);
+    
+    if (!available) {
+      // Ask user if they want to try alternative dates
+      const result = await server.server.elicitInput({
+        message: `No tables available at ${restaurant} on ${date}. Would you like to check alternative dates?`,
+        requestedSchema: {
+          type: "object",
+          properties: {
+            checkAlternatives: {
+              type: "boolean",
+              title: "Check alternative dates",
+              description: "Would you like me to check other dates?"
+            },
+            flexibleDates: {
+              type: "string",
+              title: "Date flexibility",
+              description: "How flexible are your dates?",
+              enum: ["next_day", "same_week", "next_week"],
+              enumNames: ["Next day", "Same week", "Next week"]
+            }
+          },
+          required: ["checkAlternatives"]
+        }
+      });
+
+      if (result.action === "accept" && result.content?.checkAlternatives) {
+        const alternatives = await findAlternatives(
+          restaurant, 
+          date, 
+          partySize, 
+          result.content.flexibleDates as string
+        );
+        return {
+          content: [{
+            type: "text",
+            text: `Found these alternatives: ${alternatives.join(", ")}`
+          }]
+        };
+      }
+      
+      return {
+        content: [{
+          type: "text",
+          text: "No booking made. Original date not available."
+        }]
+      };
+    }
+    
+    // Book the table
+    await makeBooking(restaurant, date, partySize);
+    return {
+      content: [{
+        type: "text",
+        text: `Booked table for ${partySize} at ${restaurant} on ${date}`
+      }]
+    };
+  }
+);
+```
+
+Client-side: Handle elicitation requests
+
+```typescript
+// This is a placeholder - implement based on your UI framework
+async function getInputFromUser(message: string, schema: any): Promise<{
+  action: "accept" | "reject" | "cancel";
+  data?: Record<string, any>;
+}> {
+  // This should be implemented depending on the app
+  throw new Error("getInputFromUser must be implemented for your platform");
+}
+
+client.setRequestHandler(ElicitRequestSchema, async (request) => {
+  const userResponse = await getInputFromUser(
+    request.params.message, 
+    request.params.requestedSchema
+  );
+  
+  return {
+    action: userResponse.action,
+    content: userResponse.action === "accept" ? userResponse.data : undefined
+  };
+});
+```
+
+**Note**: Elicitation requires client support. Clients must declare the `elicitation` capability during initialization.
+
 ### Writing MCP Clients
 
 The SDK provides a high-level client interface:
@@ -683,6 +1037,7 @@ const result = await client.callTool({
     arg1: "value"
   }
 });
+
 ```
 
 ### Proxy Authorization Requests Upstream
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 515114c..d792f01 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -15,5 +15,12 @@ export default tseslint.config(
                 { "argsIgnorePattern": "^_" }
             ]
         }
+    },
+    {
+        files: ["src/client/**/*.ts", "src/server/**/*.ts"],
+        ignores: ["**/*.test.ts"],
+        rules: {
+            "no-console": "error"
+        }
     }
 );
diff --git a/package-lock.json b/package-lock.json
index 40bad9f..5f7cb0f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@modelcontextprotocol/sdk",
-  "version": "1.11.4",
+  "version": "1.13.2",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@modelcontextprotocol/sdk",
-      "version": "1.11.4",
+      "version": "1.13.2",
       "license": "MIT",
       "dependencies": {
         "ajv": "^6.12.6",
@@ -14,6 +14,7 @@
         "cors": "^2.8.5",
         "cross-spawn": "^7.0.5",
         "eventsource": "^3.0.2",
+        "eventsource-parser": "^3.0.0",
         "express": "^5.0.1",
         "express-rate-limit": "^7.5.0",
         "pkce-challenge": "^5.0.0",
@@ -1558,6 +1559,19 @@
         "@jridgewell/sourcemap-codec": "^1.4.14"
       }
     },
+    "node_modules/@noble/hashes": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+      "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1593,6 +1607,16 @@
         "node": ">= 8"
       }
     },
+    "node_modules/@paralleldrive/cuid2": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
+      "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "^1.1.5"
+      }
+    },
     "node_modules/@sinclair/typebox": {
       "version": "0.27.8",
       "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -3688,16 +3712,19 @@
       }
     },
     "node_modules/formidable": {
-      "version": "3.5.2",
-      "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz",
-      "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==",
+      "version": "3.5.4",
+      "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
+      "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
+        "@paralleldrive/cuid2": "^2.2.2",
         "dezalgo": "^1.0.4",
-        "hexoid": "^2.0.0",
         "once": "^1.4.0"
       },
+      "engines": {
+        "node": ">=14.0.0"
+      },
       "funding": {
         "url": "https://ko-fi.com/tunnckoCore/commissions"
       }
@@ -3952,16 +3979,6 @@
         "node": ">= 0.4"
       }
     },
-    "node_modules/hexoid": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz",
-      "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/html-escaper": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
diff --git a/package.json b/package.json
index 764ce2c..31bc356 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@modelcontextprotocol/sdk",
-  "version": "1.12.2",
+  "version": "1.13.2",
   "description": "Model Context Protocol implementation for TypeScript",
   "license": "MIT",
   "author": "Anthropic, PBC (https://anthropic.com)",
@@ -36,8 +36,11 @@
   ],
   "scripts": {
     "build": "npm run build:esm && npm run build:cjs",
-    "build:esm": "tsc -p tsconfig.prod.json && echo '{\"type\": \"module\"}' > dist/esm/package.json",
-    "build:cjs": "tsc -p tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
+    "build:esm": "mkdir -p dist/esm && echo '{\"type\": \"module\"}' > dist/esm/package.json && tsc -p tsconfig.prod.json",
+    "build:esm:w": "npm run build:esm -- -w",
+    "build:cjs": "mkdir -p dist/cjs && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json && tsc -p tsconfig.cjs.json",
+    "build:cjs:w": "npm run build:cjs -- -w",
+    "examples:simple-server:w": "tsx --watch src/examples/server/simpleStreamableHttp.ts --oauth",
     "prepack": "npm run build:esm && npm run build:cjs",
     "lint": "eslint src/",
     "test": "jest",
@@ -51,6 +54,7 @@
     "cors": "^2.8.5",
     "cross-spawn": "^7.0.5",
     "eventsource": "^3.0.2",
+    "eventsource-parser": "^3.0.0",
     "express": "^5.0.1",
     "express-rate-limit": "^7.5.0",
     "pkce-challenge": "^5.0.0",
diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts
index 1b9fb07..8e77c0a 100644
--- a/src/client/auth.test.ts
+++ b/src/client/auth.test.ts
@@ -1,3 +1,4 @@
+import { LATEST_PROTOCOL_VERSION } from '../types.js';
 import {
   discoverOAuthMetadata,
   startAuthorization,
@@ -202,7 +203,145 @@ describe("OAuth Authorization", () => {
       const [url, options] = calls[0];
       expect(url.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
       expect(options.headers).toEqual({
-        "MCP-Protocol-Version": "2025-03-26"
+        "MCP-Protocol-Version": LATEST_PROTOCOL_VERSION
+      });
+    });
+
+    it("returns metadata when discovery succeeds with path", async () => {
+      mockFetch.mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => validMetadata,
+      });
+
+      const metadata = await discoverOAuthMetadata("https://auth.example.com/path/name");
+      expect(metadata).toEqual(validMetadata);
+      const calls = mockFetch.mock.calls;
+      expect(calls.length).toBe(1);
+      const [url, options] = calls[0];
+      expect(url.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server/path/name");
+      expect(options.headers).toEqual({
+        "MCP-Protocol-Version": LATEST_PROTOCOL_VERSION
+      });
+    });
+
+    it("falls back to root discovery when path-aware discovery returns 404", async () => {
+      // First call (path-aware) returns 404
+      mockFetch.mockResolvedValueOnce({
+        ok: false,
+        status: 404,
+      });
+      
+      // Second call (root fallback) succeeds
+      mockFetch.mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => validMetadata,
+      });
+
+      const metadata = await discoverOAuthMetadata("https://auth.example.com/path/name");
+      expect(metadata).toEqual(validMetadata);
+      
+      const calls = mockFetch.mock.calls;
+      expect(calls.length).toBe(2);
+      
+      // First call should be path-aware
+      const [firstUrl, firstOptions] = calls[0];
+      expect(firstUrl.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server/path/name");
+      expect(firstOptions.headers).toEqual({
+        "MCP-Protocol-Version": LATEST_PROTOCOL_VERSION
+      });
+      
+      // Second call should be root fallback
+      const [secondUrl, secondOptions] = calls[1];
+      expect(secondUrl.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
+      expect(secondOptions.headers).toEqual({
+        "MCP-Protocol-Version": LATEST_PROTOCOL_VERSION
+      });
+    });
+
+    it("returns undefined when both path-aware and root discovery return 404", async () => {
+      // First call (path-aware) returns 404
+      mockFetch.mockResolvedValueOnce({
+        ok: false,
+        status: 404,
+      });
+      
+      // Second call (root fallback) also returns 404
+      mockFetch.mockResolvedValueOnce({
+        ok: false,
+        status: 404,
+      });
+
+      const metadata = await discoverOAuthMetadata("https://auth.example.com/path/name");
+      expect(metadata).toBeUndefined();
+      
+      const calls = mockFetch.mock.calls;
+      expect(calls.length).toBe(2);
+    });
+
+    it("does not fallback when the original URL is already at root path", async () => {
+      // First call (path-aware for root) returns 404
+      mockFetch.mockResolvedValueOnce({
+        ok: false,
+        status: 404,
+      });
+
+      const metadata = await discoverOAuthMetadata("https://auth.example.com/");
+      expect(metadata).toBeUndefined();
+      
+      const calls = mockFetch.mock.calls;
+      expect(calls.length).toBe(1); // Should not attempt fallback
+      
+      const [url] = calls[0];
+      expect(url.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
+    });
+
+    it("does not fallback when the original URL has no path", async () => {
+      // First call (path-aware for no path) returns 404
+      mockFetch.mockResolvedValueOnce({
+        ok: false,
+        status: 404,
+      });
+
+      const metadata = await discoverOAuthMetadata("https://auth.example.com");
+      expect(metadata).toBeUndefined();
+      
+      const calls = mockFetch.mock.calls;
+      expect(calls.length).toBe(1); // Should not attempt fallback
+      
+      const [url] = calls[0];
+      expect(url.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
+    });
+
+    it("falls back when path-aware discovery encounters CORS error", async () => {
+      // First call (path-aware) fails with TypeError (CORS)
+      mockFetch.mockImplementationOnce(() => Promise.reject(new TypeError("CORS error")));
+      
+      // Retry path-aware without headers (simulating CORS retry)
+      mockFetch.mockResolvedValueOnce({
+        ok: false,
+        status: 404,
+      });
+      
+      // Second call (root fallback) succeeds
+      mockFetch.mockResolvedValueOnce({
+        ok: true,
+        status: 200,
+        json: async () => validMetadata,
+      });
+
+      const metadata = await discoverOAuthMetadata("https://auth.example.com/deep/path");
+      expect(metadata).toEqual(validMetadata);
+      
+      const calls = mockFetch.mock.calls;
+      expect(calls.length).toBe(3);
+      
+      // Final call should be root fallback
+      const [lastUrl, lastOptions] = calls[2];
+      expect(lastUrl.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
+      expect(lastOptions.headers).toEqual({
+        "MCP-Protocol-Version": LATEST_PROTOCOL_VERSION
       });
     });
 
@@ -324,6 +463,7 @@ describe("OAuth Authorization", () => {
           metadata: undefined,
           clientInformation: validClientInfo,
           redirectUrl: "http://localhost:3000/callback",
+          resource: new URL("https://api.example.com/mcp-server"),
         }
       );
 
@@ -338,6 +478,7 @@ describe("OAuth Authorization", () => {
       expect(authorizationUrl.searchParams.get("redirect_uri")).toBe(
         "http://localhost:3000/callback"
       );
+      expect(authorizationUrl.searchParams.get("resource")).toBe("https://api.example.com/mcp-server");
       expect(codeVerifier).toBe("test_verifier");
     });
 
@@ -465,6 +606,7 @@ describe("OAuth Authorization", () => {
         authorizationCode: "code123",
         codeVerifier: "verifier123",
         redirectUri: "http://localhost:3000/callback",
+        resource: new URL("https://api.example.com/mcp-server"),
       });
 
       expect(tokens).toEqual(validTokens);
@@ -487,6 +629,7 @@ describe("OAuth Authorization", () => {
       expect(body.get("client_id")).toBe("client123");
       expect(body.get("client_secret")).toBe("secret123");
       expect(body.get("redirect_uri")).toBe("http://localhost:3000/callback");
+      expect(body.get("resource")).toBe("https://api.example.com/mcp-server");
     });
 
     it("validates token response schema", async () => {
@@ -554,6 +697,7 @@ describe("OAuth Authorization", () => {
       const tokens = await refreshAuthorization("https://auth.example.com", {
         clientInformation: validClientInfo,
         refreshToken: "refresh123",
+        resource: new URL("https://api.example.com/mcp-server"),
       });
 
       expect(tokens).toEqual(validTokensWithNewRefreshToken);
@@ -574,6 +718,7 @@ describe("OAuth Authorization", () => {
       expect(body.get("refresh_token")).toBe("refresh123");
       expect(body.get("client_id")).toBe("client123");
       expect(body.get("client_secret")).toBe("secret123");
+      expect(body.get("resource")).toBe("https://api.example.com/mcp-server");
     });
 
     it("exchanges refresh token for new tokens and keep existing refresh token if none is returned", async () => {
@@ -807,5 +952,516 @@ describe("OAuth Authorization", () => {
         "https://resource.example.com/.well-known/oauth-authorization-server"
       );
     });
+
+    it("passes resource parameter through authorization flow", async () => {
+      // Mock successful metadata discovery - need to include protected resource metadata
+      mockFetch.mockImplementation((url) => {
+        const urlString = url.toString();
+        if (urlString.includes("/.well-known/oauth-protected-resource")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              resource: "https://api.example.com/mcp-server",
+              authorization_servers: ["https://auth.example.com"],
+            }),
+          });
+        } else if (urlString.includes("/.well-known/oauth-authorization-server")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              issuer: "https://auth.example.com",
+              authorization_endpoint: "https://auth.example.com/authorize",
+              token_endpoint: "https://auth.example.com/token",
+              response_types_supported: ["code"],
+              code_challenge_methods_supported: ["S256"],
+            }),
+          });
+        }
+        return Promise.resolve({ ok: false, status: 404 });
+      });
+
+      // Mock provider methods for authorization flow
+      (mockProvider.clientInformation as jest.Mock).mockResolvedValue({
+        client_id: "test-client",
+        client_secret: "test-secret",
+      });
+      (mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
+      (mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
+      (mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
+
+      // Call auth without authorization code (should trigger redirect)
+      const result = await auth(mockProvider, {
+        serverUrl: "https://api.example.com/mcp-server",
+      });
+
+      expect(result).toBe("REDIRECT");
+
+      // Verify the authorization URL includes the resource parameter
+      expect(mockProvider.redirectToAuthorization).toHaveBeenCalledWith(
+        expect.objectContaining({
+          searchParams: expect.any(URLSearchParams),
+        })
+      );
+
+      const redirectCall = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
+      const authUrl: URL = redirectCall[0];
+      expect(authUrl.searchParams.get("resource")).toBe("https://api.example.com/mcp-server");
+    });
+
+    it("includes resource in token exchange when authorization code is provided", async () => {
+      // Mock successful metadata discovery and token exchange - need protected resource metadata
+      mockFetch.mockImplementation((url) => {
+        const urlString = url.toString();
+
+        if (urlString.includes("/.well-known/oauth-protected-resource")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              resource: "https://api.example.com/mcp-server",
+              authorization_servers: ["https://auth.example.com"],
+            }),
+          });
+        } else if (urlString.includes("/.well-known/oauth-authorization-server")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              issuer: "https://auth.example.com",
+              authorization_endpoint: "https://auth.example.com/authorize",
+              token_endpoint: "https://auth.example.com/token",
+              response_types_supported: ["code"],
+              code_challenge_methods_supported: ["S256"],
+            }),
+          });
+        } else if (urlString.includes("/token")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              access_token: "access123",
+              token_type: "Bearer",
+              expires_in: 3600,
+              refresh_token: "refresh123",
+            }),
+          });
+        }
+
+        return Promise.resolve({ ok: false, status: 404 });
+      });
+
+      // Mock provider methods for token exchange
+      (mockProvider.clientInformation as jest.Mock).mockResolvedValue({
+        client_id: "test-client",
+        client_secret: "test-secret",
+      });
+      (mockProvider.codeVerifier as jest.Mock).mockResolvedValue("test-verifier");
+      (mockProvider.saveTokens as jest.Mock).mockResolvedValue(undefined);
+
+      // Call auth with authorization code
+      const result = await auth(mockProvider, {
+        serverUrl: "https://api.example.com/mcp-server",
+        authorizationCode: "auth-code-123",
+      });
+
+      expect(result).toBe("AUTHORIZED");
+
+      // Find the token exchange call
+      const tokenCall = mockFetch.mock.calls.find(call =>
+        call[0].toString().includes("/token")
+      );
+      expect(tokenCall).toBeDefined();
+
+      const body = tokenCall![1].body as URLSearchParams;
+      expect(body.get("resource")).toBe("https://api.example.com/mcp-server");
+      expect(body.get("code")).toBe("auth-code-123");
+    });
+
+    it("includes resource in token refresh", async () => {
+      // Mock successful metadata discovery and token refresh - need protected resource metadata
+      mockFetch.mockImplementation((url) => {
+        const urlString = url.toString();
+
+        if (urlString.includes("/.well-known/oauth-protected-resource")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              resource: "https://api.example.com/mcp-server",
+              authorization_servers: ["https://auth.example.com"],
+            }),
+          });
+        } else if (urlString.includes("/.well-known/oauth-authorization-server")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              issuer: "https://auth.example.com",
+              authorization_endpoint: "https://auth.example.com/authorize",
+              token_endpoint: "https://auth.example.com/token",
+              response_types_supported: ["code"],
+              code_challenge_methods_supported: ["S256"],
+            }),
+          });
+        } else if (urlString.includes("/token")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              access_token: "new-access123",
+              token_type: "Bearer",
+              expires_in: 3600,
+            }),
+          });
+        }
+
+        return Promise.resolve({ ok: false, status: 404 });
+      });
+
+      // Mock provider methods for token refresh
+      (mockProvider.clientInformation as jest.Mock).mockResolvedValue({
+        client_id: "test-client",
+        client_secret: "test-secret",
+      });
+      (mockProvider.tokens as jest.Mock).mockResolvedValue({
+        access_token: "old-access",
+        refresh_token: "refresh123",
+      });
+      (mockProvider.saveTokens as jest.Mock).mockResolvedValue(undefined);
+
+      // Call auth with existing tokens (should trigger refresh)
+      const result = await auth(mockProvider, {
+        serverUrl: "https://api.example.com/mcp-server",
+      });
+
+      expect(result).toBe("AUTHORIZED");
+
+      // Find the token refresh call
+      const tokenCall = mockFetch.mock.calls.find(call =>
+        call[0].toString().includes("/token")
+      );
+      expect(tokenCall).toBeDefined();
+
+      const body = tokenCall![1].body as URLSearchParams;
+      expect(body.get("resource")).toBe("https://api.example.com/mcp-server");
+      expect(body.get("grant_type")).toBe("refresh_token");
+      expect(body.get("refresh_token")).toBe("refresh123");
+    });
+
+    it("skips default PRM resource validation when custom validateResourceURL is provided", async () => {
+      const mockValidateResourceURL = jest.fn().mockResolvedValue(undefined);
+      const providerWithCustomValidation = {
+        ...mockProvider,
+        validateResourceURL: mockValidateResourceURL,
+      };
+
+      // Mock protected resource metadata with mismatched resource URL
+      // This would normally throw an error in default validation, but should be skipped
+      mockFetch.mockImplementation((url) => {
+        const urlString = url.toString();
+
+        if (urlString.includes("/.well-known/oauth-protected-resource")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              resource: "https://different-resource.example.com/mcp-server", // Mismatched resource
+              authorization_servers: ["https://auth.example.com"],
+            }),
+          });
+        } else if (urlString.includes("/.well-known/oauth-authorization-server")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              issuer: "https://auth.example.com",
+              authorization_endpoint: "https://auth.example.com/authorize",
+              token_endpoint: "https://auth.example.com/token",
+              response_types_supported: ["code"],
+              code_challenge_methods_supported: ["S256"],
+            }),
+          });
+        }
+
+        return Promise.resolve({ ok: false, status: 404 });
+      });
+
+      // Mock provider methods
+      (providerWithCustomValidation.clientInformation as jest.Mock).mockResolvedValue({
+        client_id: "test-client",
+        client_secret: "test-secret",
+      });
+      (providerWithCustomValidation.tokens as jest.Mock).mockResolvedValue(undefined);
+      (providerWithCustomValidation.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
+      (providerWithCustomValidation.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
+
+      // Call auth - should succeed despite resource mismatch because custom validation overrides default
+      const result = await auth(providerWithCustomValidation, {
+        serverUrl: "https://api.example.com/mcp-server",
+      });
+
+      expect(result).toBe("REDIRECT");
+
+      // Verify custom validation method was called
+      expect(mockValidateResourceURL).toHaveBeenCalledWith(
+        new URL("https://api.example.com/mcp-server"),
+        "https://different-resource.example.com/mcp-server"
+      );
+    });
+
+    it("uses prefix of server URL from PRM resource as resource parameter", async () => {
+      // Mock successful metadata discovery with resource URL that is a prefix of requested URL
+      mockFetch.mockImplementation((url) => {
+        const urlString = url.toString();
+
+        if (urlString.includes("/.well-known/oauth-protected-resource")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              // Resource is a prefix of the requested server URL
+              resource: "https://api.example.com/",
+              authorization_servers: ["https://auth.example.com"],
+            }),
+          });
+        } else if (urlString.includes("/.well-known/oauth-authorization-server")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              issuer: "https://auth.example.com",
+              authorization_endpoint: "https://auth.example.com/authorize",
+              token_endpoint: "https://auth.example.com/token",
+              response_types_supported: ["code"],
+              code_challenge_methods_supported: ["S256"],
+            }),
+          });
+        }
+
+        return Promise.resolve({ ok: false, status: 404 });
+      });
+
+      // Mock provider methods
+      (mockProvider.clientInformation as jest.Mock).mockResolvedValue({
+        client_id: "test-client",
+        client_secret: "test-secret",
+      });
+      (mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
+      (mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
+      (mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
+
+      // Call auth with a URL that has the resource as prefix
+      const result = await auth(mockProvider, {
+        serverUrl: "https://api.example.com/mcp-server/endpoint",
+      });
+
+      expect(result).toBe("REDIRECT");
+
+      // Verify the authorization URL includes the resource parameter from PRM
+      expect(mockProvider.redirectToAuthorization).toHaveBeenCalledWith(
+        expect.objectContaining({
+          searchParams: expect.any(URLSearchParams),
+        })
+      );
+
+      const redirectCall = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
+      const authUrl: URL = redirectCall[0];
+      // Should use the PRM's resource value, not the full requested URL
+      expect(authUrl.searchParams.get("resource")).toBe("https://api.example.com/");
+    });
+
+    it("excludes resource parameter when Protected Resource Metadata is not present", async () => {
+      // Mock metadata discovery where protected resource metadata is not available (404)
+      // but authorization server metadata is available
+      mockFetch.mockImplementation((url) => {
+        const urlString = url.toString();
+
+        if (urlString.includes("/.well-known/oauth-protected-resource")) {
+          // Protected resource metadata not available
+          return Promise.resolve({
+            ok: false,
+            status: 404,
+          });
+        } else if (urlString.includes("/.well-known/oauth-authorization-server")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              issuer: "https://auth.example.com",
+              authorization_endpoint: "https://auth.example.com/authorize",
+              token_endpoint: "https://auth.example.com/token",
+              response_types_supported: ["code"],
+              code_challenge_methods_supported: ["S256"],
+            }),
+          });
+        }
+
+        return Promise.resolve({ ok: false, status: 404 });
+      });
+
+      // Mock provider methods
+      (mockProvider.clientInformation as jest.Mock).mockResolvedValue({
+        client_id: "test-client",
+        client_secret: "test-secret",
+      });
+      (mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
+      (mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
+      (mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
+
+      // Call auth - should not include resource parameter
+      const result = await auth(mockProvider, {
+        serverUrl: "https://api.example.com/mcp-server",
+      });
+
+      expect(result).toBe("REDIRECT");
+
+      // Verify the authorization URL does NOT include the resource parameter
+      expect(mockProvider.redirectToAuthorization).toHaveBeenCalledWith(
+        expect.objectContaining({
+          searchParams: expect.any(URLSearchParams),
+        })
+      );
+
+      const redirectCall = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
+      const authUrl: URL = redirectCall[0];
+      // Resource parameter should not be present when PRM is not available
+      expect(authUrl.searchParams.has("resource")).toBe(false);
+    });
+
+    it("excludes resource parameter in token exchange when Protected Resource Metadata is not present", async () => {
+      // Mock metadata discovery - no protected resource metadata, but auth server metadata available
+      mockFetch.mockImplementation((url) => {
+        const urlString = url.toString();
+
+        if (urlString.includes("/.well-known/oauth-protected-resource")) {
+          return Promise.resolve({
+            ok: false,
+            status: 404,
+          });
+        } else if (urlString.includes("/.well-known/oauth-authorization-server")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              issuer: "https://auth.example.com",
+              authorization_endpoint: "https://auth.example.com/authorize",
+              token_endpoint: "https://auth.example.com/token",
+              response_types_supported: ["code"],
+              code_challenge_methods_supported: ["S256"],
+            }),
+          });
+        } else if (urlString.includes("/token")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              access_token: "access123",
+              token_type: "Bearer",
+              expires_in: 3600,
+              refresh_token: "refresh123",
+            }),
+          });
+        }
+
+        return Promise.resolve({ ok: false, status: 404 });
+      });
+
+      // Mock provider methods for token exchange
+      (mockProvider.clientInformation as jest.Mock).mockResolvedValue({
+        client_id: "test-client",
+        client_secret: "test-secret",
+      });
+      (mockProvider.codeVerifier as jest.Mock).mockResolvedValue("test-verifier");
+      (mockProvider.saveTokens as jest.Mock).mockResolvedValue(undefined);
+
+      // Call auth with authorization code
+      const result = await auth(mockProvider, {
+        serverUrl: "https://api.example.com/mcp-server",
+        authorizationCode: "auth-code-123",
+      });
+
+      expect(result).toBe("AUTHORIZED");
+
+      // Find the token exchange call
+      const tokenCall = mockFetch.mock.calls.find(call =>
+        call[0].toString().includes("/token")
+      );
+      expect(tokenCall).toBeDefined();
+
+      const body = tokenCall![1].body as URLSearchParams;
+      // Resource parameter should not be present when PRM is not available
+      expect(body.has("resource")).toBe(false);
+      expect(body.get("code")).toBe("auth-code-123");
+    });
+
+    it("excludes resource parameter in token refresh when Protected Resource Metadata is not present", async () => {
+      // Mock metadata discovery - no protected resource metadata, but auth server metadata available
+      mockFetch.mockImplementation((url) => {
+        const urlString = url.toString();
+
+        if (urlString.includes("/.well-known/oauth-protected-resource")) {
+          return Promise.resolve({
+            ok: false,
+            status: 404,
+          });
+        } else if (urlString.includes("/.well-known/oauth-authorization-server")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              issuer: "https://auth.example.com",
+              authorization_endpoint: "https://auth.example.com/authorize",
+              token_endpoint: "https://auth.example.com/token",
+              response_types_supported: ["code"],
+              code_challenge_methods_supported: ["S256"],
+            }),
+          });
+        } else if (urlString.includes("/token")) {
+          return Promise.resolve({
+            ok: true,
+            status: 200,
+            json: async () => ({
+              access_token: "new-access123",
+              token_type: "Bearer",
+              expires_in: 3600,
+            }),
+          });
+        }
+
+        return Promise.resolve({ ok: false, status: 404 });
+      });
+
+      // Mock provider methods for token refresh
+      (mockProvider.clientInformation as jest.Mock).mockResolvedValue({
+        client_id: "test-client",
+        client_secret: "test-secret",
+      });
+      (mockProvider.tokens as jest.Mock).mockResolvedValue({
+        access_token: "old-access",
+        refresh_token: "refresh123",
+      });
+      (mockProvider.saveTokens as jest.Mock).mockResolvedValue(undefined);
+
+      // Call auth with existing tokens (should trigger refresh)
+      const result = await auth(mockProvider, {
+        serverUrl: "https://api.example.com/mcp-server",
+      });
+
+      expect(result).toBe("AUTHORIZED");
+
+      // Find the token refresh call
+      const tokenCall = mockFetch.mock.calls.find(call =>
+        call[0].toString().includes("/token")
+      );
+      expect(tokenCall).toBeDefined();
+
+      const body = tokenCall![1].body as URLSearchParams;
+      // Resource parameter should not be present when PRM is not available
+      expect(body.has("resource")).toBe(false);
+      expect(body.get("grant_type")).toBe("refresh_token");
+      expect(body.get("refresh_token")).toBe("refresh123");
+    });
   });
 });
diff --git a/src/client/auth.ts b/src/client/auth.ts
index 7a91eb2..3769057 100644
--- a/src/client/auth.ts
+++ b/src/client/auth.ts
@@ -2,6 +2,7 @@ import pkceChallenge from "pkce-challenge";
 import { LATEST_PROTOCOL_VERSION } from "../types.js";
 import type { OAuthClientMetadata, OAuthClientInformation, OAuthTokens, OAuthMetadata, OAuthClientInformationFull, OAuthProtectedResourceMetadata } from "../shared/auth.js";
 import { OAuthClientInformationFullSchema, OAuthMetadataSchema, OAuthProtectedResourceMetadataSchema, OAuthTokensSchema } from "../shared/auth.js";
+import { checkResourceAllowed, resourceUrlFromServerUrl } from "../shared/auth-utils.js";
 
 /**
  * Implements an end-to-end OAuth client to be used with one MCP server.
@@ -71,6 +72,15 @@ export interface OAuthClientProvider {
    * the authorization result.
    */
   codeVerifier(): string | Promise<string>;
+
+  /**
+   * If defined, overrides the selection and validation of the
+   * RFC 8707 Resource Indicator. If left undefined, default
+   * validation behavior will be used.
+   *
+   * Implementations must verify the returned resource matches the MCP server.
+   */
+  validateResourceURL?(serverUrl: string | URL, resource?: string): Promise<URL | undefined>;
 }
 
 export type AuthResult = "AUTHORIZED" | "REDIRECT";
@@ -99,18 +109,19 @@ export async function auth(
     scope?: string;
     resourceMetadataUrl?: URL }): Promise<AuthResult> {
 
+  let resourceMetadata: OAuthProtectedResourceMetadata | undefined;
   let authorizationServerUrl = serverUrl;
   try {
-    const resourceMetadata = await discoverOAuthProtectedResourceMetadata(
-      resourceMetadataUrl || serverUrl);
-
+    resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, {resourceMetadataUrl});
     if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
       authorizationServerUrl = resourceMetadata.authorization_servers[0];
     }
-  } catch (error) {
-    console.warn("Could not load OAuth Protected Resource metadata, falling back to /.well-known/oauth-authorization-server", error)
+  } catch {
+    // Ignore errors and fall back to /.well-known/oauth-authorization-server
   }
 
+  const resource: URL | undefined = await selectResourceURL(serverUrl, provider, resourceMetadata);
+
   const metadata = await discoverOAuthMetadata(authorizationServerUrl);
 
   // Handle client registration if needed
@@ -142,6 +153,7 @@ export async function auth(
       authorizationCode,
       codeVerifier,
       redirectUri: provider.redirectUrl,
+      resource,
     });
 
     await provider.saveTokens(tokens);
@@ -158,12 +170,13 @@ export async function auth(
         metadata,
         clientInformation,
         refreshToken: tokens.refresh_token,
+        resource,
       });
 
       await provider.saveTokens(newTokens);
       return "AUTHORIZED";
-    } catch (error) {
-      console.error("Could not refresh OAuth tokens:", error);
+    } catch {
+      // Could not refresh OAuth tokens
     }
   }
 
@@ -176,6 +189,7 @@ export async function auth(
     state,
     redirectUrl: provider.redirectUrl,
     scope: scope || provider.clientMetadata.scope,
+    resource,
   });
 
   await provider.saveCodeVerifier(codeVerifier);
@@ -183,6 +197,27 @@ export async function auth(
   return "REDIRECT";
 }
 
+export async function selectResourceURL(serverUrl: string| URL, provider: OAuthClientProvider, resourceMetadata?: OAuthProtectedResourceMetadata): Promise<URL | undefined> {
+  const defaultResource = resourceUrlFromServerUrl(serverUrl);
+
+  // If provider has custom validation, delegate to it
+  if (provider.validateResourceURL) {
+    return await provider.validateResourceURL(defaultResource, resourceMetadata?.resource);
+  }
+
+  // Only include resource parameter when Protected Resource Metadata is present
+  if (!resourceMetadata) {
+    return undefined;
+  }
+
+  // Validate that the metadata's resource is compatible with our request
+  if (!checkResourceAllowed({ requestedResource: defaultResource, configuredResource: resourceMetadata.resource })) {
+    throw new Error(`Protected resource ${resourceMetadata.resource} does not match expected ${defaultResource} (or origin)`);
+  }
+  // Prefer the resource from metadata since it's what the server is telling us to request
+  return new URL(resourceMetadata.resource);
+}
+
 /**
  * Extract resource_metadata from response header.
  */
@@ -195,7 +230,6 @@ export function extractResourceMetadataUrl(res: Response): URL | undefined {
 
   const [type, scheme] = authenticateHeader.split(' ');
   if (type.toLowerCase() !== 'bearer' || !scheme) {
-    console.log("Invalid WWW-Authenticate header format, expected 'Bearer'");
     return undefined;
   }
   const regex = /resource_metadata="([^"]*)"/;
@@ -208,7 +242,6 @@ export function extractResourceMetadataUrl(res: Response): URL | undefined {
   try {
     return new URL(match[1]);
   } catch {
-    console.log("Invalid resource metadata url: ", match[1]);
     return undefined;
   }
 }
@@ -265,28 +298,82 @@ export async function discoverOAuthProtectedResourceMetadata(
  * If the server returns a 404 for the well-known endpoint, this function will
  * return `undefined`. Any other errors will be thrown as exceptions.
  */
-export async function discoverOAuthMetadata(
-  authorizationServerUrl: string | URL,
-  opts?: { protocolVersion?: string },
-): Promise<OAuthMetadata | undefined> {
-  const url = new URL("/.well-known/oauth-authorization-server", authorizationServerUrl);
-  let response: Response;
+/**
+ * Helper function to handle fetch with CORS retry logic
+ */
+async function fetchWithCorsRetry(
+  url: URL,
+  headers: Record<string, string>,
+): Promise<Response> {
   try {
-    response = await fetch(url, {
-      headers: {
-        "MCP-Protocol-Version": opts?.protocolVersion ?? LATEST_PROTOCOL_VERSION
-      }
-    });
+    return await fetch(url, { headers });
   } catch (error) {
-    // CORS errors come back as TypeError
+    // CORS errors come back as TypeError, retry without headers
     if (error instanceof TypeError) {
-      response = await fetch(url);
-    } else {
-      throw error;
+      return await fetch(url);
     }
+    throw error;
   }
+}
 
-  if (response.status === 404) {
+/**
+ * Constructs the well-known path for OAuth metadata discovery
+ */
+function buildWellKnownPath(pathname: string): string {
+  let wellKnownPath = `/.well-known/oauth-authorization-server${pathname}`;
+  if (pathname.endsWith('/')) {
+    // Strip trailing slash from pathname to avoid double slashes
+    wellKnownPath = wellKnownPath.slice(0, -1);
+  }
+  return wellKnownPath;
+}
+
+/**
+ * Tries to discover OAuth metadata at a specific URL
+ */
+async function tryMetadataDiscovery(
+  url: URL,
+  protocolVersion: string,
+): Promise<Response> {
+  const headers = {
+    "MCP-Protocol-Version": protocolVersion
+  };
+  return await fetchWithCorsRetry(url, headers);
+}
+
+/**
+ * Determines if fallback to root discovery should be attempted
+ */
+function shouldAttemptFallback(response: Response, pathname: string): boolean {
+  return response.status === 404 && pathname !== '/';
+}
+
+export async function discoverOAuthMetadata(
+  authorizationServerUrl: string | URL,
+  opts?: { protocolVersion?: string },
+): Promise<OAuthMetadata | undefined> {
+  const issuer = new URL(authorizationServerUrl);
+  const protocolVersion = opts?.protocolVersion ?? LATEST_PROTOCOL_VERSION;
+
+  // Try path-aware discovery first (RFC 8414 compliant)
+  const wellKnownPath = buildWellKnownPath(issuer.pathname);
+  const pathAwareUrl = new URL(wellKnownPath, issuer);
+  let response = await tryMetadataDiscovery(pathAwareUrl, protocolVersion);
+
+  // If path-aware discovery fails with 404, try fallback to root discovery
+  if (shouldAttemptFallback(response, issuer.pathname)) {
+    try {
+      const rootUrl = new URL("/.well-known/oauth-authorization-server", issuer);
+      response = await tryMetadataDiscovery(rootUrl, protocolVersion);
+
+      if (response.status === 404) {
+        return undefined;
+      }
+    } catch {
+      // If fallback fails, return undefined
+      return undefined;
+    }
+  } else if (response.status === 404) {
     return undefined;
   }
 
@@ -310,12 +397,14 @@ export async function startAuthorization(
     redirectUrl,
     scope,
     state,
+    resource,
   }: {
     metadata?: OAuthMetadata;
     clientInformation: OAuthClientInformation;
     redirectUrl: string | URL;
     scope?: string;
     state?: string;
+    resource?: URL;
   },
 ): Promise<{ authorizationUrl: URL; codeVerifier: string }> {
   const responseType = "code";
@@ -365,6 +454,10 @@ export async function startAuthorization(
     authorizationUrl.searchParams.set("scope", scope);
   }
 
+  if (resource) {
+    authorizationUrl.searchParams.set("resource", resource.href);
+  }
+
   return { authorizationUrl, codeVerifier };
 }
 
@@ -379,12 +472,14 @@ export async function exchangeAuthorization(
     authorizationCode,
     codeVerifier,
     redirectUri,
+    resource,
   }: {
     metadata?: OAuthMetadata;
     clientInformation: OAuthClientInformation;
     authorizationCode: string;
     codeVerifier: string;
     redirectUri: string | URL;
+    resource?: URL;
   },
 ): Promise<OAuthTokens> {
   const grantType = "authorization_code";
@@ -418,6 +513,10 @@ export async function exchangeAuthorization(
     params.set("client_secret", clientInformation.client_secret);
   }
 
+  if (resource) {
+    params.set("resource", resource.href);
+  }
+
   const response = await fetch(tokenUrl, {
     method: "POST",
     headers: {
@@ -442,10 +541,12 @@ export async function refreshAuthorization(
     metadata,
     clientInformation,
     refreshToken,
+    resource,
   }: {
     metadata?: OAuthMetadata;
     clientInformation: OAuthClientInformation;
     refreshToken: string;
+    resource?: URL;
   },
 ): Promise<OAuthTokens> {
   const grantType = "refresh_token";
@@ -477,6 +578,10 @@ export async function refreshAuthorization(
     params.set("client_secret", clientInformation.client_secret);
   }
 
+  if (resource) {
+    params.set("resource", resource.href);
+  }
+
   const response = await fetch(tokenUrl, {
     method: "POST",
     headers: {
diff --git a/src/client/index.test.ts b/src/client/index.test.ts
index bbfa80f..abd0c34 100644
--- a/src/client/index.test.ts
+++ b/src/client/index.test.ts
@@ -14,6 +14,7 @@ import {
   ListToolsRequestSchema,
   CallToolRequestSchema,
   CreateMessageRequestSchema,
+  ElicitRequestSchema,
   ListRootsRequestSchema,
   ErrorCode,
 } from "../types.js";
@@ -597,6 +598,43 @@ test("should only allow setRequestHandler for declared capabilities", () => {
   }).toThrow("Client does not support roots capability");
 });
 
+test("should allow setRequestHandler for declared elicitation capability", () => {
+  const client = new Client(
+    {
+      name: "test-client",
+      version: "1.0.0",
+    },
+    {
+      capabilities: {
+        elicitation: {},
+      },
+    },
+  );
+
+  // This should work because elicitation is a declared capability
+  expect(() => {
+    client.setRequestHandler(ElicitRequestSchema, () => ({
+      action: "accept",
+      content: {
+        username: "test-user",
+        confirmed: true,
+      },
+    }));
+  }).not.toThrow();
+
+  // This should throw because sampling is not a declared capability
+  expect(() => {
+    client.setRequestHandler(CreateMessageRequestSchema, () => ({
+      model: "test-model",
+      role: "assistant",
+      content: {
+        type: "text",
+        text: "Test response",
+      },
+    }));
+  }).toThrow("Client does not support sampling capability");
+});
+
 /***
  * Test: Type Checking
  * Test that custom request/notification/result schemas can be used with the Client class.
diff --git a/src/client/index.ts b/src/client/index.ts
index 98618a1..3e8d8ec 100644
--- a/src/client/index.ts
+++ b/src/client/index.ts
@@ -165,6 +165,10 @@ export class Client<
 
       this._serverCapabilities = result.capabilities;
       this._serverVersion = result.serverInfo;
+      // HTTP transports must set the protocol version in each header after initialization.
+      if (transport.setProtocolVersion) {
+        transport.setProtocolVersion(result.protocolVersion);
+      }
 
       this._instructions = result.instructions;
 
@@ -303,6 +307,14 @@ export class Client<
         }
         break;
 
+      case "elicitation/create":
+        if (!this._capabilities.elicitation) {
+          throw new Error(
+            `Client does not support elicitation capability (required for ${method})`,
+          );
+        }
+        break;
+
       case "roots/list":
         if (!this._capabilities.roots) {
           throw new Error(
@@ -474,8 +486,8 @@ export class Client<
         try {
           const validator = this._ajv.compile(tool.outputSchema);
           this._cachedToolOutputValidators.set(tool.name, validator);
-        } catch (error) {
-          console.warn(`Failed to compile output schema for tool ${tool.name}: ${error}`);
+        } catch {
+          // Ignore schema compilation errors
         }
       }
     }
diff --git a/src/client/sse.test.ts b/src/client/sse.test.ts
index 714e1fd..3cb4e8a 100644
--- a/src/client/sse.test.ts
+++ b/src/client/sse.test.ts
@@ -398,7 +398,7 @@ describe("SSEClientTransport", () => {
             'Content-Type': 'application/json',
           })
           .end(JSON.stringify({
-            resource: "https://resource.example.com",
+            resource: resourceBaseUrl.href,
             authorization_servers: [`${authBaseUrl}`],
           }));
           return;
@@ -450,7 +450,7 @@ describe("SSEClientTransport", () => {
                 'Content-Type': 'application/json',
               })
               .end(JSON.stringify({
-                resource: "https://resource.example.com",
+                resource: resourceBaseUrl.href,
                 authorization_servers: [`${authBaseUrl}`],
               }));
               return;
@@ -601,7 +601,7 @@ describe("SSEClientTransport", () => {
             'Content-Type': 'application/json',
           })
           .end(JSON.stringify({
-            resource: "https://resource.example.com",
+            resource: resourceBaseUrl.href,
             authorization_servers: [`${authBaseUrl}`],
           }));
           return;
@@ -723,7 +723,7 @@ describe("SSEClientTransport", () => {
             'Content-Type': 'application/json',
           })
           .end(JSON.stringify({
-            resource: "https://resource.example.com",
+            resource: resourceBaseUrl.href,
             authorization_servers: [`${authBaseUrl}`],
           }));
           return;
@@ -851,7 +851,7 @@ describe("SSEClientTransport", () => {
             'Content-Type': 'application/json',
           })
           .end(JSON.stringify({
-            resource: "https://resource.example.com",
+            resource: resourceBaseUrl.href,
             authorization_servers: [`${authBaseUrl}`],
           }));
           return;
diff --git a/src/client/sse.ts b/src/client/sse.ts
index 7939e8c..2546d50 100644
--- a/src/client/sse.ts
+++ b/src/client/sse.ts
@@ -62,6 +62,7 @@ export class SSEClientTransport implements Transport {
   private _eventSourceInit?: EventSourceInit;
   private _requestInit?: RequestInit;
   private _authProvider?: OAuthClientProvider;
+  private _protocolVersion?: string;
 
   onclose?: () => void;
   onerror?: (error: Error) => void;
@@ -99,35 +100,52 @@ export class SSEClientTransport implements Transport {
   }
 
   private async _commonHeaders(): Promise<HeadersInit> {
-    const headers: HeadersInit = { ...this._requestInit?.headers };
+    const headers = {
+      ...this._requestInit?.headers,
+    } as HeadersInit & Record<string, string>;
     if (this._authProvider) {
       const tokens = await this._authProvider.tokens();
       if (tokens) {
-        (headers as Record<string, string>)["Authorization"] = `Bearer ${tokens.access_token}`;
+        headers["Authorization"] = `Bearer ${tokens.access_token}`;
       }
     }
+    if (this._protocolVersion) {
+      headers["mcp-protocol-version"] = this._protocolVersion;
+    }
 
     return headers;
   }
 
   private _startOrAuth(): Promise<void> {
+    const fetchImpl = (this?._eventSourceInit?.fetch || fetch) as typeof fetch
     return new Promise((resolve, reject) => {
       this._eventSource = new EventSource(
         this._url.href,
-        this._eventSourceInit ?? {
-          fetch: (url, init) => this._commonHeaders().then((headers) => fetch(url, {
-            ...init,
-            headers: {
-              ...headers,
-              Accept: "text/event-stream"
+        {
+          ...this._eventSourceInit,
+          fetch: async (url, init) => {
+            const headers = await this._commonHeaders()
+            const response = await fetchImpl(url, {
+              ...init,
+              headers: new Headers({
+                ...headers,
+                Accept: "text/event-stream"
+              })
+            })
+
+            if (response.status === 401 && response.headers.has('www-authenticate')) {
+              this._resourceMetadataUrl = extractResourceMetadataUrl(response);
             }
-          })),
+
+            return response
+          },
         },
       );
       this._abortController = new AbortController();
 
       this._eventSource.onerror = (event) => {
         if (event.code === 401 && this._authProvider) {
+
           this._authThenStart().then(resolve, reject);
           return;
         }
@@ -214,7 +232,7 @@ export class SSEClientTransport implements Transport {
 
     try {
       const commonHeaders = await this._commonHeaders();
-      const headers = new Headers({ ...commonHeaders, ...this._requestInit?.headers });
+      const headers = new Headers(commonHeaders);
       headers.set("content-type", "application/json");
       const init = {
         ...this._requestInit,
@@ -249,4 +267,8 @@ export class SSEClientTransport implements Transport {
       throw error;
     }
   }
+
+  setProtocolVersion(version: string): void {
+    this._protocolVersion = version;
+  }
 }
diff --git a/src/client/stdio.ts b/src/client/stdio.ts
index 9e35293..e84810a 100644
--- a/src/client/stdio.ts
+++ b/src/client/stdio.ts
@@ -56,6 +56,7 @@ export const DEFAULT_INHERITED_ENV_VARS =
         "TEMP",
         "USERNAME",
         "USERPROFILE",
+        "PROGRAMFILES",
       ]
     : /* list inspired by the default env inheritance of sudo */
       ["HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"];
diff --git a/src/client/streamableHttp.test.ts b/src/client/streamableHttp.test.ts
index f748a2b..11dfe7d 100644
--- a/src/client/streamableHttp.test.ts
+++ b/src/client/streamableHttp.test.ts
@@ -476,6 +476,37 @@ describe("StreamableHTTPClientTransport", () => {
     expect(global.fetch).toHaveBeenCalledTimes(2);
   });
 
+  it("should always send specified custom headers (Headers class)", async () => {
+    const requestInit = {
+      headers: new Headers({
+        "X-Custom-Header": "CustomValue"
+      })
+    };
+    transport = new StreamableHTTPClientTransport(new URL("http://localhost:1234/mcp"), {
+      requestInit: requestInit
+    });
+
+    let actualReqInit: RequestInit = {};
+
+    ((global.fetch as jest.Mock)).mockImplementation(
+      async (_url, reqInit) => {
+        actualReqInit = reqInit;
+        return new Response(null, { status: 200, headers: { "content-type": "text/event-stream" } });
+      }
+    );
+
+    await transport.start();
+
+    await transport["_startOrAuthSse"]({});
+    expect((actualReqInit.headers as Headers).get("x-custom-header")).toBe("CustomValue");
+
+    (requestInit.headers as Headers).set("X-Custom-Header","SecondCustomValue");
+
+    await transport.send({ jsonrpc: "2.0", method: "test", params: {} } as JSONRPCMessage);
+    expect((actualReqInit.headers as Headers).get("x-custom-header")).toBe("SecondCustomValue");
+
+    expect(global.fetch).toHaveBeenCalledTimes(2);
+  });
 
   it("should have exponential backoff with configurable maxRetries", () => {
     // This test verifies the maxRetries and backoff calculation directly
diff --git a/src/client/streamableHttp.ts b/src/client/streamableHttp.ts
index 1bcfbb2..7307844 100644
--- a/src/client/streamableHttp.ts
+++ b/src/client/streamableHttp.ts
@@ -124,6 +124,7 @@ export class StreamableHTTPClientTransport implements Transport {
   private _authProvider?: OAuthClientProvider;
   private _sessionId?: string;
   private _reconnectionOptions: StreamableHTTPReconnectionOptions;
+  private _protocolVersion?: string;
 
   onclose?: () => void;
   onerror?: (error: Error) => void;
@@ -162,7 +163,7 @@ export class StreamableHTTPClientTransport implements Transport {
   }
 
   private async _commonHeaders(): Promise<Headers> {
-    const headers: HeadersInit = {};
+    const headers: HeadersInit & Record<string, string> = {};
     if (this._authProvider) {
       const tokens = await this._authProvider.tokens();
       if (tokens) {
@@ -173,10 +174,16 @@ export class StreamableHTTPClientTransport implements Transport {
     if (this._sessionId) {
       headers["mcp-session-id"] = this._sessionId;
     }
+    if (this._protocolVersion) {
+      headers["mcp-protocol-version"] = this._protocolVersion;
+    }
+
+    const extraHeaders = this._normalizeHeaders(this._requestInit?.headers);
 
-    return new Headers(
-      { ...headers, ...this._requestInit?.headers }
-    );
+    return new Headers({
+      ...headers,
+      ...extraHeaders,
+    });
   }
 
 
@@ -242,6 +249,20 @@ export class StreamableHTTPClientTransport implements Transport {
 
   }
 
+    private _normalizeHeaders(headers: HeadersInit | undefined): Record<string, string> {
+    if (!headers) return {};
+    
+    if (headers instanceof Headers) {
+      return Object.fromEntries(headers.entries());
+    }
+    
+    if (Array.isArray(headers)) {
+      return Object.fromEntries(headers);
+    }
+    
+    return { ...headers as Record<string, string> };
+  }
+
   /**
    * Schedule a reconnection attempt with exponential backoff
    *
@@ -516,4 +537,11 @@ export class StreamableHTTPClientTransport implements Transport {
       throw error;
     }
   }
+
+  setProtocolVersion(version: string): void {
+    this._protocolVersion = version;
+  }
+  get protocolVersion(): string | undefined {
+    return this._protocolVersion;
+  }
 }
diff --git a/src/examples/README.md b/src/examples/README.md
index 68e1ece..ac92e8d 100644
--- a/src/examples/README.md
+++ b/src/examples/README.md
@@ -76,6 +76,9 @@ npx tsx src/examples/server/simpleStreamableHttp.ts
 
 # To add a demo of authentication to this example, use:
 npx tsx src/examples/server/simpleStreamableHttp.ts --oauth
+
+# To mitigate impersonation risks, enable strict Resource Identifier verification:
+npx tsx src/examples/server/simpleStreamableHttp.ts --oauth --oauth-strict
 ```
 
 ##### JSON Response Mode Server
diff --git a/src/examples/client/simpleStreamableHttp.ts b/src/examples/client/simpleStreamableHttp.ts
index 19d32bb..02db131 100644
--- a/src/examples/client/simpleStreamableHttp.ts
+++ b/src/examples/client/simpleStreamableHttp.ts
@@ -14,7 +14,13 @@ import {
   ListResourcesResultSchema,
   LoggingMessageNotificationSchema,
   ResourceListChangedNotificationSchema,
+  ElicitRequestSchema,
+  ResourceLink,
+  ReadResourceRequest,
+  ReadResourceResultSchema,
 } from '../../types.js';
+import { getDisplayName } from '../../shared/metadataUtils.js';
+import Ajv from "ajv";
 
 // Create readline interface for user input
 const readline = createInterface({
@@ -54,11 +60,13 @@ function printHelp(): void {
   console.log('  call-tool <name> [args]    - Call a tool with optional JSON arguments');
   console.log('  greet [name]               - Call the greet tool');
   console.log('  multi-greet [name]         - Call the multi-greet tool with notifications');
+  console.log('  collect-info [type]        - Test elicitation with collect-user-info tool (contact/preferences/feedback)');
   console.log('  start-notifications [interval] [count] - Start periodic notifications');
   console.log('  run-notifications-tool-with-resumability [interval] [count] - Run notification tool with resumability');
   console.log('  list-prompts               - List available prompts');
   console.log('  get-prompt [name] [args]   - Get a prompt with optional JSON arguments');
   console.log('  list-resources             - List available resources');
+  console.log('  read-resource <uri>        - Read a specific resource by URI');
   console.log('  help                       - Show this help');
   console.log('  quit                       - Exit the program');
 }
@@ -115,6 +123,10 @@ function commandLoop(): void {
           await callMultiGreetTool(args[1] || 'MCP User');
           break;
 
+        case 'collect-info':
+          await callCollectInfoTool(args[1] || 'contact');
+          break;
+
         case 'start-notifications': {
           const interval = args[1] ? parseInt(args[1], 10) : 2000;
           const count = args[2] ? parseInt(args[2], 10) : 10;
@@ -154,6 +166,14 @@ function commandLoop(): void {
           await listResources();
           break;
 
+        case 'read-resource':
+          if (args.length < 2) {
+            console.log('Usage: read-resource <uri>');
+          } else {
+            await readResource(args[1]);
+          }
+          break;
+
         case 'help':
           printHelp();
           break;
@@ -191,15 +211,212 @@ async function connect(url?: string): Promise<void> {
   console.log(`Connecting to ${serverUrl}...`);
 
   try {
-    // Create a new client
+    // Create a new client with elicitation capability
     client = new Client({
       name: 'example-client',
       version: '1.0.0'
+    }, {
+      capabilities: {
+        elicitation: {},
+      },
     });
     client.onerror = (error) => {
       console.error('\x1b[31mClient error:', error, '\x1b[0m');
     }
 
+    // Set up elicitation request handler with proper validation
+    client.setRequestHandler(ElicitRequestSchema, async (request) => {
+      console.log('\n🔔 Elicitation Request Received:');
+      console.log(`Message: ${request.params.message}`);
+      console.log('Requested Schema:');
+      console.log(JSON.stringify(request.params.requestedSchema, null, 2));
+
+      const schema = request.params.requestedSchema;
+      const properties = schema.properties;
+      const required = schema.required || [];
+
+      // Set up AJV validator for the requested schema
+      const ajv = new Ajv();
+      const validate = ajv.compile(schema);
+
+      let attempts = 0;
+      const maxAttempts = 3;
+
+      while (attempts < maxAttempts) {
+        attempts++;
+        console.log(`\nPlease provide the following information (attempt ${attempts}/${maxAttempts}):`);
+
+        const content: Record<string, unknown> = {};
+        let inputCancelled = false;
+
+        // Collect input for each field
+        for (const [fieldName, fieldSchema] of Object.entries(properties)) {
+          const field = fieldSchema as {
+            type?: string;
+            title?: string;
+            description?: string;
+            default?: unknown;
+            enum?: string[];
+            minimum?: number;
+            maximum?: number;
+            minLength?: number;
+            maxLength?: number;
+            format?: string;
+          };
+
+          const isRequired = required.includes(fieldName);
+          let prompt = `${field.title || fieldName}`;
+
+          // Add helpful information to the prompt
+          if (field.description) {
+            prompt += ` (${field.description})`;
+          }
+          if (field.enum) {
+            prompt += ` [options: ${field.enum.join(', ')}]`;
+          }
+          if (field.type === 'number' || field.type === 'integer') {
+            if (field.minimum !== undefined && field.maximum !== undefined) {
+              prompt += ` [${field.minimum}-${field.maximum}]`;
+            } else if (field.minimum !== undefined) {
+              prompt += ` [min: ${field.minimum}]`;
+            } else if (field.maximum !== undefined) {
+              prompt += ` [max: ${field.maximum}]`;
+            }
+          }
+          if (field.type === 'string' && field.format) {
+            prompt += ` [format: ${field.format}]`;
+          }
+          if (isRequired) {
+            prompt += ' *required*';
+          }
+          if (field.default !== undefined) {
+            prompt += ` [default: ${field.default}]`;
+          }
+
+          prompt += ': ';
+
+          const answer = await new Promise<string>((resolve) => {
+            readline.question(prompt, (input) => {
+              resolve(input.trim());
+            });
+          });
+
+          // Check for cancellation
+          if (answer.toLowerCase() === 'cancel' || answer.toLowerCase() === 'c') {
+            inputCancelled = true;
+            break;
+          }
+
+          // Parse and validate the input
+          try {
+            if (answer === '' && field.default !== undefined) {
+              content[fieldName] = field.default;
+            } else if (answer === '' && !isRequired) {
+              // Skip optional empty fields
+              continue;
+            } else if (answer === '') {
+              throw new Error(`${fieldName} is required`);
+            } else {
+              // Parse the value based on type
+              let parsedValue: unknown;
+
+              if (field.type === 'boolean') {
+                parsedValue = answer.toLowerCase() === 'true' || answer.toLowerCase() === 'yes' || answer === '1';
+              } else if (field.type === 'number') {
+                parsedValue = parseFloat(answer);
+                if (isNaN(parsedValue as number)) {
+                  throw new Error(`${fieldName} must be a valid number`);
+                }
+              } else if (field.type === 'integer') {
+                parsedValue = parseInt(answer, 10);
+                if (isNaN(parsedValue as number)) {
+                  throw new Error(`${fieldName} must be a valid integer`);
+                }
+              } else if (field.enum) {
+                if (!field.enum.includes(answer)) {
+                  throw new Error(`${fieldName} must be one of: ${field.enum.join(', ')}`);
+                }
+                parsedValue = answer;
+              } else {
+                parsedValue = answer;
+              }
+
+              content[fieldName] = parsedValue;
+            }
+          } catch (error) {
+            console.log(`❌ Error: ${error}`);
+            // Continue to next attempt
+            break;
+          }
+        }
+
+        if (inputCancelled) {
+          return { action: 'cancel' };
+        }
+
+        // If we didn't complete all fields due to an error, try again
+        if (Object.keys(content).length !== Object.keys(properties).filter(name =>
+          required.includes(name) || content[name] !== undefined
+        ).length) {
+          if (attempts < maxAttempts) {
+            console.log('Please try again...');
+            continue;
+          } else {
+            console.log('Maximum attempts reached. Declining request.');
+            return { action: 'reject' };
+          }
+        }
+
+        // Validate the complete object against the schema
+        const isValid = validate(content);
+
+        if (!isValid) {
+          console.log('❌ Validation errors:');
+          validate.errors?.forEach(error => {
+            console.log(`  - ${error.dataPath || 'root'}: ${error.message}`);
+          });
+
+          if (attempts < maxAttempts) {
+            console.log('Please correct the errors and try again...');
+            continue;
+          } else {
+            console.log('Maximum attempts reached. Declining request.');
+            return { action: 'reject' };
+          }
+        }
+
+        // Show the collected data and ask for confirmation
+        console.log('\n✅ Collected data:');
+        console.log(JSON.stringify(content, null, 2));
+
+        const confirmAnswer = await new Promise<string>((resolve) => {
+          readline.question('\nSubmit this information? (yes/no/cancel): ', (input) => {
+            resolve(input.trim().toLowerCase());
+          });
+        });
+
+
+        if (confirmAnswer === 'yes' || confirmAnswer === 'y') {
+          return {
+            action: 'accept',
+            content,
+          };
+        } else if (confirmAnswer === 'cancel' || confirmAnswer === 'c') {
+          return { action: 'cancel' };
+        } else if (confirmAnswer === 'no' || confirmAnswer === 'n') {
+          if (attempts < maxAttempts) {
+            console.log('Please re-enter the information...');
+            continue;
+          } else {
+            return { action: 'reject' };
+          }
+        }
+      }
+
+      console.log('Maximum attempts reached. Declining request.');
+      return { action: 'reject' };
+    });
+
     transport = new StreamableHTTPClientTransport(
       new URL(serverUrl),
       {
@@ -317,7 +534,7 @@ async function listTools(): Promise<void> {
       console.log('  No tools available');
     } else {
       for (const tool of toolsResult.tools) {
-        console.log(`  - ${tool.name}: ${tool.description}`);
+        console.log(`  - id: ${tool.name}, name: ${getDisplayName(tool)}, description: ${tool.description}`);
       }
     }
   } catch (error) {
@@ -344,13 +561,37 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<vo
     const result = await client.request(request, CallToolResultSchema);
 
     console.log('Tool result:');
+    const resourceLinks: ResourceLink[] = [];
+
     result.content.forEach(item => {
       if (item.type === 'text') {
         console.log(`  ${item.text}`);
+      } else if (item.type === 'resource_link') {
+        const resourceLink = item as ResourceLink;
+        resourceLinks.push(resourceLink);
+        console.log(`  📁 Resource Link: ${resourceLink.name}`);
+        console.log(`     URI: ${resourceLink.uri}`);
+        if (resourceLink.mimeType) {
+          console.log(`     Type: ${resourceLink.mimeType}`);
+        }
+        if (resourceLink.description) {
+          console.log(`     Description: ${resourceLink.description}`);
+        }
+      } else if (item.type === 'resource') {
+        console.log(`  [Embedded Resource: ${item.resource.uri}]`);
+      } else if (item.type === 'image') {
+        console.log(`  [Image: ${item.mimeType}]`);
+      } else if (item.type === 'audio') {
+        console.log(`  [Audio: ${item.mimeType}]`);
       } else {
-        console.log(`  ${item.type} content:`, item);
+        console.log(`  [Unknown content type]:`, item);
       }
     });
+
+    // Offer to read resource links
+    if (resourceLinks.length > 0) {
+      console.log(`\nFound ${resourceLinks.length} resource link(s). Use 'read-resource <uri>' to read their content.`);
+    }
   } catch (error) {
     console.log(`Error calling tool ${name}: ${error}`);
   }
@@ -366,6 +607,11 @@ async function callMultiGreetTool(name: string): Promise<void> {
   await callTool('multi-greet', { name });
 }
 
+async function callCollectInfoTool(infoType: string): Promise<void> {
+  console.log(`Testing elicitation with collect-user-info tool (${infoType})...`);
+  await callTool('collect-user-info', { infoType });
+}
+
 async function startNotifications(interval: number, count: number): Promise<void> {
   console.log(`Starting notification stream: interval=${interval}ms, count=${count || 'unlimited'}`);
   await callTool('start-notification-stream', { interval, count });
@@ -380,7 +626,7 @@ async function runNotificationsToolWithResumability(interval: number, count: num
   try {
     console.log(`Starting notification stream with resumability: interval=${interval}ms, count=${count || 'unlimited'}`);
     console.log(`Using resumption token: ${notificationsToolLastEventId || 'none'}`);
-    
+
     const request: CallToolRequest = {
       method: 'tools/call',
       params: {
@@ -393,7 +639,7 @@ async function runNotificationsToolWithResumability(interval: number, count: num
       notificationsToolLastEventId = event;
       console.log(`Updated resumption token: ${event}`);
     };
-    
+
     const result = await client.request(request, CallToolResultSchema, {
       resumptionToken: notificationsToolLastEventId,
       onresumptiontoken: onLastEventIdUpdate
@@ -429,7 +675,7 @@ async function listPrompts(): Promise<void> {
       console.log('  No prompts available');
     } else {
       for (const prompt of promptsResult.prompts) {
-        console.log(`  - ${prompt.name}: ${prompt.description}`);
+        console.log(`  - id: ${prompt.name}, name: ${getDisplayName(prompt)}, description: ${prompt.description}`);
       }
     }
   } catch (error) {
@@ -480,7 +726,7 @@ async function listResources(): Promise<void> {
       console.log('  No resources available');
     } else {
       for (const resource of resourcesResult.resources) {
-        console.log(`  - ${resource.name}: ${resource.uri}`);
+        console.log(`  - id: ${resource.name}, name: ${getDisplayName(resource)}, description: ${resource.uri}`);
       }
     }
   } catch (error) {
@@ -488,6 +734,42 @@ async function listResources(): Promise<void> {
   }
 }
 
+async function readResource(uri: string): Promise<void> {
+  if (!client) {
+    console.log('Not connected to server.');
+    return;
+  }
+
+  try {
+    const request: ReadResourceRequest = {
+      method: 'resources/read',
+      params: { uri }
+    };
+
+    console.log(`Reading resource: ${uri}`);
+    const result = await client.request(request, ReadResourceResultSchema);
+
+    console.log('Resource contents:');
+    for (const content of result.contents) {
+      console.log(`  URI: ${content.uri}`);
+      if (content.mimeType) {
+        console.log(`  Type: ${content.mimeType}`);
+      }
+
+      if ('text' in content && typeof content.text === 'string') {
+        console.log('  Content:');
+        console.log('  ---');
+        console.log(content.text.split('\n').map((line: string) => '  ' + line).join('\n'));
+        console.log('  ---');
+      } else if ('blob' in content && typeof content.blob === 'string') {
+        console.log(`  [Binary data: ${content.blob.length} bytes]`);
+      }
+    }
+  } catch (error) {
+    console.log(`Error reading resource ${uri}: ${error}`);
+  }
+}
+
 async function cleanup(): Promise<void> {
   if (client && transport) {
     try {
diff --git a/src/examples/server/demoInMemoryOAuthProvider.ts b/src/examples/server/demoInMemoryOAuthProvider.ts
index 024208d..274a504 100644
--- a/src/examples/server/demoInMemoryOAuthProvider.ts
+++ b/src/examples/server/demoInMemoryOAuthProvider.ts
@@ -1,10 +1,11 @@
 import { randomUUID } from 'node:crypto';
 import { AuthorizationParams, OAuthServerProvider } from '../../server/auth/provider.js';
 import { OAuthRegisteredClientsStore } from '../../server/auth/clients.js';
-import { OAuthClientInformationFull, OAuthMetadata, OAuthTokens } from 'src/shared/auth.js';
+import { OAuthClientInformationFull, OAuthMetadata, OAuthTokens } from '../../shared/auth.js';
 import express, { Request, Response } from "express";
-import { AuthInfo } from 'src/server/auth/types.js';
-import { createOAuthMetadata, mcpAuthRouter } from 'src/server/auth/router.js';
+import { AuthInfo } from '../../server/auth/types.js';
+import { createOAuthMetadata, mcpAuthRouter } from '../../server/auth/router.js';
+import { resourceUrlFromServerUrl } from '../../shared/auth-utils.js';
 
 
 export class DemoInMemoryClientsStore implements OAuthRegisteredClientsStore {
@@ -35,6 +36,8 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider {
     client: OAuthClientInformationFull}>();
   private tokens = new Map<string, AuthInfo>();
 
+  constructor(private validateResource?: (resource?: URL) => boolean) {}
+
   async authorize(
     client: OAuthClientInformationFull,
     params: AuthorizationParams,
@@ -89,6 +92,10 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider {
       throw new Error(`Authorization code was not issued to this client, ${codeData.client.client_id} != ${client.client_id}`);
     }
 
+    if (this.validateResource && !this.validateResource(codeData.params.resource)) {
+      throw new Error(`Invalid resource: ${codeData.params.resource}`);
+    }
+
     this.codes.delete(authorizationCode);
     const token = randomUUID();
 
@@ -97,7 +104,8 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider {
       clientId: client.client_id,
       scopes: codeData.params.scopes || [],
       expiresAt: Date.now() + 3600000, // 1 hour
-      type: 'access'
+      resource: codeData.params.resource,
+      type: 'access',
     };
 
     this.tokens.set(token, tokenData);
@@ -113,7 +121,8 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider {
   async exchangeRefreshToken(
     _client: OAuthClientInformationFull,
     _refreshToken: string,
-    _scopes?: string[]
+    _scopes?: string[],
+    _resource?: URL
   ): Promise<OAuthTokens> {
     throw new Error('Not implemented for example demo');
   }
@@ -129,18 +138,26 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider {
       clientId: tokenData.clientId,
       scopes: tokenData.scopes,
       expiresAt: Math.floor(tokenData.expiresAt / 1000),
+      resource: tokenData.resource,
     };
   }
 }
 
 
-export const setupAuthServer = (authServerUrl: URL): OAuthMetadata => {
+export const setupAuthServer = ({authServerUrl, mcpServerUrl, strictResource}: {authServerUrl: URL, mcpServerUrl: URL, strictResource: boolean}): OAuthMetadata => {
   // Create separate auth server app
   // NOTE: This is a separate app on a separate port to illustrate
   // how to separate an OAuth Authorization Server from a Resource
   // server in the SDK. The SDK is not intended to be provide a standalone
   // authorization server.
-  const provider = new DemoInMemoryAuthProvider();
+
+  const validateResource = strictResource ? (resource?: URL) => {
+    if (!resource) return false;
+    const expectedResource = resourceUrlFromServerUrl(mcpServerUrl);
+    return resource.toString() === expectedResource.toString();
+  } : undefined;
+
+  const provider = new DemoInMemoryAuthProvider(validateResource);
   const authApp = express();
   authApp.use(express.json());
   // For introspection requests
@@ -168,7 +185,8 @@ export const setupAuthServer = (authServerUrl: URL): OAuthMetadata => {
         active: true,
         client_id: tokenInfo.clientId,
         scope: tokenInfo.scopes.join(' '),
-        exp: tokenInfo.expiresAt
+        exp: tokenInfo.expiresAt,
+        aud: tokenInfo.resource,
       });
       return
     } catch (error) {
diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts
index 6c33119..fea0eec 100644
--- a/src/examples/server/simpleStreamableHttp.ts
+++ b/src/examples/server/simpleStreamableHttp.ts
@@ -5,27 +5,32 @@ import { McpServer } from '../../server/mcp.js';
 import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
 import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js';
 import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js';
-import { CallToolResult, GetPromptResult, isInitializeRequest, ReadResourceResult } from '../../types.js';
+import { CallToolResult, GetPromptResult, isInitializeRequest, PrimitiveSchemaDefinition, ReadResourceResult, ResourceLink } from '../../types.js';
 import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
 import { setupAuthServer } from './demoInMemoryOAuthProvider.js';
 import { OAuthMetadata } from 'src/shared/auth.js';
+import { checkResourceAllowed } from 'src/shared/auth-utils.js';
 
 // Check for OAuth flag
 const useOAuth = process.argv.includes('--oauth');
+const strictOAuth = process.argv.includes('--oauth-strict');
 
 // Create an MCP server with implementation details
 const getServer = () => {
   const server = new McpServer({
     name: 'simple-streamable-http-server',
-    version: '1.0.0',
+    version: '1.0.0'
   }, { capabilities: { logging: {} } });
 
   // Register a simple tool that returns a greeting
-  server.tool(
+  server.registerTool(
     'greet',
-    'A simple greeting tool',
     {
-      name: z.string().describe('Name to greet'),
+      title: 'Greeting Tool',  // Display name for UI
+      description: 'A simple greeting tool',
+      inputSchema: {
+        name: z.string().describe('Name to greet'),
+      },
     },
     async ({ name }): Promise<CallToolResult> => {
       return {
@@ -83,13 +88,165 @@ const getServer = () => {
       };
     }
   );
+  // Register a tool that demonstrates elicitation (user input collection)
+  // This creates a closure that captures the server instance
+  server.tool(
+    'collect-user-info',
+    'A tool that collects user information through elicitation',
+    {
+      infoType: z.enum(['contact', 'preferences', 'feedback']).describe('Type of information to collect'),
+    },
+    async ({ infoType }): Promise<CallToolResult> => {
+      let message: string;
+      let requestedSchema: {
+        type: 'object';
+        properties: Record<string, PrimitiveSchemaDefinition>;
+        required?: string[];
+      };
+
+      switch (infoType) {
+        case 'contact':
+          message = 'Please provide your contact information';
+          requestedSchema = {
+            type: 'object',
+            properties: {
+              name: {
+                type: 'string',
+                title: 'Full Name',
+                description: 'Your full name',
+              },
+              email: {
+                type: 'string',
+                title: 'Email Address',
+                description: 'Your email address',
+                format: 'email',
+              },
+              phone: {
+                type: 'string',
+                title: 'Phone Number',
+                description: 'Your phone number (optional)',
+              },
+            },
+            required: ['name', 'email'],
+          };
+          break;
+        case 'preferences':
+          message = 'Please set your preferences';
+          requestedSchema = {
+            type: 'object',
+            properties: {
+              theme: {
+                type: 'string',
+                title: 'Theme',
+                description: 'Choose your preferred theme',
+                enum: ['light', 'dark', 'auto'],
+                enumNames: ['Light', 'Dark', 'Auto'],
+              },
+              notifications: {
+                type: 'boolean',
+                title: 'Enable Notifications',
+                description: 'Would you like to receive notifications?',
+                default: true,
+              },
+              frequency: {
+                type: 'string',
+                title: 'Notification Frequency',
+                description: 'How often would you like notifications?',
+                enum: ['daily', 'weekly', 'monthly'],
+                enumNames: ['Daily', 'Weekly', 'Monthly'],
+              },
+            },
+            required: ['theme'],
+          };
+          break;
+        case 'feedback':
+          message = 'Please provide your feedback';
+          requestedSchema = {
+            type: 'object',
+            properties: {
+              rating: {
+                type: 'integer',
+                title: 'Rating',
+                description: 'Rate your experience (1-5)',
+                minimum: 1,
+                maximum: 5,
+              },
+              comments: {
+                type: 'string',
+                title: 'Comments',
+                description: 'Additional comments (optional)',
+                maxLength: 500,
+              },
+              recommend: {
+                type: 'boolean',
+                title: 'Would you recommend this?',
+                description: 'Would you recommend this to others?',
+              },
+            },
+            required: ['rating', 'recommend'],
+          };
+          break;
+        default:
+          throw new Error(`Unknown info type: ${infoType}`);
+      }
+
+      try {
+        // Use the underlying server instance to elicit input from the client
+        const result = await server.server.elicitInput({
+          message,
+          requestedSchema,
+        });
+
+        if (result.action === 'accept') {
+          return {
+            content: [
+              {
+                type: 'text',
+                text: `Thank you! Collected ${infoType} information: ${JSON.stringify(result.content, null, 2)}`,
+              },
+            ],
+          };
+        } else if (result.action === 'reject') {
+          return {
+            content: [
+              {
+                type: 'text',
+                text: `No information was collected. User rejected ${infoType} information request.`,
+              },
+            ],
+          };
+        } else {
+          return {
+            content: [
+              {
+                type: 'text',
+                text: `Information collection was cancelled by the user.`,
+              },
+            ],
+          };
+        }
+      } catch (error) {
+        return {
+          content: [
+            {
+              type: 'text',
+              text: `Error collecting ${infoType} information: ${error}`,
+            },
+          ],
+        };
+      }
+    }
+  );
 
-  // Register a simple prompt
-  server.prompt(
+  // Register a simple prompt with title
+  server.registerPrompt(
     'greeting-template',
-    'A simple greeting prompt template',
     {
-      name: z.string().describe('Name to include in greeting'),
+      title: 'Greeting Template',  // Display name for UI
+      description: 'A simple greeting prompt template',
+      argsSchema: {
+        name: z.string().describe('Name to include in greeting'),
+      },
     },
     async ({ name }): Promise<GetPromptResult> => {
       return {
@@ -148,10 +305,14 @@ const getServer = () => {
   );
 
   // Create a simple resource at a fixed URI
-  server.resource(
+  server.registerResource(
     'greeting-resource',
     'https://example.com/greetings/default',
-    { mimeType: 'text/plain' },
+    {
+      title: 'Default Greeting',  // Display name for UI
+      description: 'A simple greeting resource',
+      mimeType: 'text/plain'
+    },
     async (): Promise<ReadResourceResult> => {
       return {
         contents: [
@@ -163,6 +324,99 @@ const getServer = () => {
       };
     }
   );
+
+  // Create additional resources for ResourceLink demonstration
+  server.registerResource(
+    'example-file-1',
+    'file:///example/file1.txt',
+    {
+      title: 'Example File 1',
+      description: 'First example file for ResourceLink demonstration',
+      mimeType: 'text/plain'
+    },
+    async (): Promise<ReadResourceResult> => {
+      return {
+        contents: [
+          {
+            uri: 'file:///example/file1.txt',
+            text: 'This is the content of file 1',
+          },
+        ],
+      };
+    }
+  );
+
+  server.registerResource(
+    'example-file-2',
+    'file:///example/file2.txt',
+    {
+      title: 'Example File 2',
+      description: 'Second example file for ResourceLink demonstration',
+      mimeType: 'text/plain'
+    },
+    async (): Promise<ReadResourceResult> => {
+      return {
+        contents: [
+          {
+            uri: 'file:///example/file2.txt',
+            text: 'This is the content of file 2',
+          },
+        ],
+      };
+    }
+  );
+
+  // Register a tool that returns ResourceLinks
+  server.registerTool(
+    'list-files',
+    {
+      title: 'List Files with ResourceLinks',
+      description: 'Returns a list of files as ResourceLinks without embedding their content',
+      inputSchema: {
+        includeDescriptions: z.boolean().optional().describe('Whether to include descriptions in the resource links'),
+      },
+    },
+    async ({ includeDescriptions = true }): Promise<CallToolResult> => {
+      const resourceLinks: ResourceLink[] = [
+        {
+          type: 'resource_link',
+          uri: 'https://example.com/greetings/default',
+          name: 'Default Greeting',
+          mimeType: 'text/plain',
+          ...(includeDescriptions && { description: 'A simple greeting resource' })
+        },
+        {
+          type: 'resource_link',
+          uri: 'file:///example/file1.txt',
+          name: 'Example File 1',
+          mimeType: 'text/plain',
+          ...(includeDescriptions && { description: 'First example file for ResourceLink demonstration' })
+        },
+        {
+          type: 'resource_link',
+          uri: 'file:///example/file2.txt',
+          name: 'Example File 2',
+          mimeType: 'text/plain',
+          ...(includeDescriptions && { description: 'Second example file for ResourceLink demonstration' })
+        }
+      ];
+
+      return {
+        content: [
+          {
+            type: 'text',
+            text: 'Here are the available files as resource links:',
+          },
+          ...resourceLinks,
+          {
+            type: 'text',
+            text: '\nYou can read any of these resources using their URI.',
+          }
+        ],
+      };
+    }
+  );
+
   return server;
 };
 
@@ -176,10 +430,10 @@ app.use(express.json());
 let authMiddleware = null;
 if (useOAuth) {
   // Create auth middleware for MCP endpoints
-  const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}`);
+  const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`);
   const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`);
 
-  const oauthMetadata: OAuthMetadata = setupAuthServer(authServerUrl);
+  const oauthMetadata: OAuthMetadata = setupAuthServer({authServerUrl, mcpServerUrl, strictResource: strictOAuth});
 
   const tokenVerifier = {
     verifyAccessToken: async (token: string) => {
@@ -206,6 +460,15 @@ if (useOAuth) {
 
       const data = await response.json();
 
+      if (strictOAuth) {
+        if (!data.aud) {
+          throw new Error(`Resource Indicator (RFC8707) missing`);
+        }
+        if (!checkResourceAllowed({ requestedResource: data.aud, configuredResource: mcpServerUrl })) {
+          throw new Error(`Expected resource indicator ${mcpServerUrl}, got: ${data.aud}`);
+        }
+      }
+
       // Convert the response to AuthInfo format
       return {
         token,
@@ -225,7 +488,7 @@ if (useOAuth) {
 
   authMiddleware = requireBearerAuth({
     verifier: tokenVerifier,
-    requiredScopes: ['mcp:tools'],
+    requiredScopes: [],
     resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
   });
 }
@@ -235,15 +498,13 @@ const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
 
 // MCP POST endpoint with optional auth
 const mcpPostHandler = async (req: Request, res: Response) => {
-  console.log('Received MCP request:', req.body);
+  const sessionId = req.headers['mcp-session-id'] as string | undefined;
+  console.log(sessionId? `Received MCP request for session: ${sessionId}`: 'Received MCP request:', req.body);
   if (useOAuth && req.auth) {
     console.log('Authenticated user:', req.auth);
   }
   try {
-    // Check for existing session ID
-    const sessionId = req.headers['mcp-session-id'] as string | undefined;
     let transport: StreamableHTTPServerTransport;
-
     if (sessionId && transports[sessionId]) {
       // Reuse existing transport
       transport = transports[sessionId];
diff --git a/src/server/auth/handlers/authorize.test.ts b/src/server/auth/handlers/authorize.test.ts
index e921d5e..438db6a 100644
--- a/src/server/auth/handlers/authorize.test.ts
+++ b/src/server/auth/handlers/authorize.test.ts
@@ -276,6 +276,34 @@ describe('Authorization Handler', () => {
     });
   });
 
+  describe('Resource parameter validation', () => {
+    it('propagates resource parameter', async () => {
+      const mockProviderWithResource = jest.spyOn(mockProvider, 'authorize');
+      
+      const response = await supertest(app)
+        .get('/authorize')
+        .query({
+          client_id: 'valid-client',
+          redirect_uri: 'https://example.com/callback',
+          response_type: 'code',
+          code_challenge: 'challenge123',
+          code_challenge_method: 'S256',
+          resource: 'https://api.example.com/resource'
+        });
+
+      expect(response.status).toBe(302);
+      expect(mockProviderWithResource).toHaveBeenCalledWith(
+        validClient,
+        expect.objectContaining({
+          resource: new URL('https://api.example.com/resource'),
+          redirectUri: 'https://example.com/callback',
+          codeChallenge: 'challenge123'
+        }),
+        expect.any(Object)
+      );
+    });
+  });
+
   describe('Successful authorization', () => {
     it('handles successful authorization with all parameters', async () => {
       const response = await supertest(app)
diff --git a/src/server/auth/handlers/authorize.ts b/src/server/auth/handlers/authorize.ts
index 3e9a336..126ce00 100644
--- a/src/server/auth/handlers/authorize.ts
+++ b/src/server/auth/handlers/authorize.ts
@@ -35,6 +35,7 @@ const RequestAuthorizationParamsSchema = z.object({
   code_challenge_method: z.literal("S256"),
   scope: z.string().optional(),
   state: z.string().optional(),
+  resource: z.string().url().optional(),
 });
 
 export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: AuthorizationHandlerOptions): RequestHandler {
@@ -98,7 +99,6 @@ export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: A
         const status = error instanceof ServerError ? 500 : 400;
         res.status(status).json(error.toResponseObject());
       } else {
-        console.error("Unexpected error looking up client:", error);
         const serverError = new ServerError("Internal Server Error");
         res.status(500).json(serverError.toResponseObject());
       }
@@ -115,7 +115,7 @@ export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: A
         throw new InvalidRequestError(parseResult.error.message);
       }
 
-      const { scope, code_challenge } = parseResult.data;
+      const { scope, code_challenge, resource } = parseResult.data;
       state = parseResult.data.state;
 
       // Validate scopes
@@ -138,13 +138,13 @@ export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: A
         scopes: requestedScopes,
         redirectUri: redirect_uri,
         codeChallenge: code_challenge,
+        resource: resource ? new URL(resource) : undefined,
       }, res);
     } catch (error) {
       // Post-redirect errors - redirect with error parameters
       if (error instanceof OAuthError) {
         res.redirect(302, createErrorRedirect(redirect_uri, error, state));
       } else {
-        console.error("Unexpected error during authorization:", error);
         const serverError = new ServerError("Internal Server Error");
         res.redirect(302, createErrorRedirect(redirect_uri, serverError, state));
       }
diff --git a/src/server/auth/handlers/register.ts b/src/server/auth/handlers/register.ts
index 30b7cdf..c313734 100644
--- a/src/server/auth/handlers/register.ts
+++ b/src/server/auth/handlers/register.ts
@@ -104,7 +104,6 @@ export function clientRegistrationHandler({
         const status = error instanceof ServerError ? 500 : 400;
         res.status(status).json(error.toResponseObject());
       } else {
-        console.error("Unexpected error registering client:", error);
         const serverError = new ServerError("Internal Server Error");
         res.status(500).json(serverError.toResponseObject());
       }
diff --git a/src/server/auth/handlers/revoke.ts b/src/server/auth/handlers/revoke.ts
index 95e8b4b..0d1b30e 100644
--- a/src/server/auth/handlers/revoke.ts
+++ b/src/server/auth/handlers/revoke.ts
@@ -9,7 +9,7 @@ import {
   InvalidRequestError,
   ServerError,
   TooManyRequestsError,
-  OAuthError
+  OAuthError,
 } from "../errors.js";
 
 export type RevocationHandlerOptions = {
@@ -21,7 +21,10 @@ export type RevocationHandlerOptions = {
   rateLimit?: Partial<RateLimitOptions> | false;
 };
 
-export function revocationHandler({ provider, rateLimit: rateLimitConfig }: RevocationHandlerOptions): RequestHandler {
+export function revocationHandler({
+  provider,
+  rateLimit: rateLimitConfig,
+}: RevocationHandlerOptions): RequestHandler {
   if (!provider.revokeToken) {
     throw new Error("Auth provider does not support revoking tokens");
   }
@@ -37,21 +40,25 @@ export function revocationHandler({ provider, rateLimit: rateLimitConfig }: Revo
 
   // Apply rate limiting unless explicitly disabled
   if (rateLimitConfig !== false) {
-    router.use(rateLimit({
-      windowMs: 15 * 60 * 1000, // 15 minutes
-      max: 50, // 50 requests per windowMs
-      standardHeaders: true,
-      legacyHeaders: false,
-      message: new TooManyRequestsError('You have exceeded the rate limit for token revocation requests').toResponseObject(),
-      ...rateLimitConfig
-    }));
+    router.use(
+      rateLimit({
+        windowMs: 15 * 60 * 1000, // 15 minutes
+        max: 50, // 50 requests per windowMs
+        standardHeaders: true,
+        legacyHeaders: false,
+        message: new TooManyRequestsError(
+          "You have exceeded the rate limit for token revocation requests"
+        ).toResponseObject(),
+        ...rateLimitConfig,
+      })
+    );
   }
 
   // Authenticate and extract client details
   router.use(authenticateClient({ clientsStore: provider.clientsStore }));
 
   router.post("/", async (req, res) => {
-    res.setHeader('Cache-Control', 'no-store');
+    res.setHeader("Cache-Control", "no-store");
 
     try {
       const parseResult = OAuthTokenRevocationRequestSchema.safeParse(req.body);
@@ -62,7 +69,6 @@ export function revocationHandler({ provider, rateLimit: rateLimitConfig }: Revo
       const client = req.client;
       if (!client) {
         // This should never happen
-        console.error("Missing client information after authentication");
         throw new ServerError("Internal Server Error");
       }
 
@@ -73,7 +79,6 @@ export function revocationHandler({ provider, rateLimit: rateLimitConfig }: Revo
         const status = error instanceof ServerError ? 500 : 400;
         res.status(status).json(error.toResponseObject());
       } else {
-        console.error("Unexpected error revoking token:", error);
         const serverError = new ServerError("Internal Server Error");
         res.status(500).json(serverError.toResponseObject());
       }
diff --git a/src/server/auth/handlers/token.test.ts b/src/server/auth/handlers/token.test.ts
index c165fe7..4b7fae0 100644
--- a/src/server/auth/handlers/token.test.ts
+++ b/src/server/auth/handlers/token.test.ts
@@ -264,12 +264,14 @@ describe('Token Handler', () => {
     });
 
     it('returns tokens for valid code exchange', async () => {
+      const mockExchangeCode = jest.spyOn(mockProvider, 'exchangeAuthorizationCode');
       const response = await supertest(app)
         .post('/token')
         .type('form')
         .send({
           client_id: 'valid-client',
           client_secret: 'valid-secret',
+          resource: 'https://api.example.com/resource',
           grant_type: 'authorization_code',
           code: 'valid_code',
           code_verifier: 'valid_verifier'
@@ -280,6 +282,13 @@ describe('Token Handler', () => {
       expect(response.body.token_type).toBe('bearer');
       expect(response.body.expires_in).toBe(3600);
       expect(response.body.refresh_token).toBe('mock_refresh_token');
+      expect(mockExchangeCode).toHaveBeenCalledWith(
+        validClient,
+        'valid_code',
+        undefined, // code_verifier is undefined after PKCE validation
+        undefined, // redirect_uri
+        new URL('https://api.example.com/resource') // resource parameter
+      );
     });
 
     it('passes through code verifier when using proxy provider', async () => {
@@ -440,12 +449,14 @@ describe('Token Handler', () => {
     });
 
     it('returns new tokens for valid refresh token', async () => {
+      const mockExchangeRefresh = jest.spyOn(mockProvider, 'exchangeRefreshToken');
       const response = await supertest(app)
         .post('/token')
         .type('form')
         .send({
           client_id: 'valid-client',
           client_secret: 'valid-secret',
+          resource: 'https://api.example.com/resource',
           grant_type: 'refresh_token',
           refresh_token: 'valid_refresh_token'
         });
@@ -455,6 +466,12 @@ describe('Token Handler', () => {
       expect(response.body.token_type).toBe('bearer');
       expect(response.body.expires_in).toBe(3600);
       expect(response.body.refresh_token).toBe('new_mock_refresh_token');
+      expect(mockExchangeRefresh).toHaveBeenCalledWith(
+        validClient,
+        'valid_refresh_token',
+        undefined, // scopes
+        new URL('https://api.example.com/resource') // resource parameter
+      );
     });
 
     it('respects requested scopes on refresh', async () => {
diff --git a/src/server/auth/handlers/token.ts b/src/server/auth/handlers/token.ts
index eadbd75..b2ab743 100644
--- a/src/server/auth/handlers/token.ts
+++ b/src/server/auth/handlers/token.ts
@@ -32,11 +32,13 @@ const AuthorizationCodeGrantSchema = z.object({
   code: z.string(),
   code_verifier: z.string(),
   redirect_uri: z.string().optional(),
+  resource: z.string().url().optional(),
 });
 
 const RefreshTokenGrantSchema = z.object({
   refresh_token: z.string(),
   scope: z.string().optional(),
+  resource: z.string().url().optional(),
 });
 
 export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHandlerOptions): RequestHandler {
@@ -78,7 +80,6 @@ export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHand
       const client = req.client;
       if (!client) {
         // This should never happen
-        console.error("Missing client information after authentication");
         throw new ServerError("Internal Server Error");
       }
 
@@ -89,7 +90,7 @@ export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHand
             throw new InvalidRequestError(parseResult.error.message);
           }
 
-          const { code, code_verifier, redirect_uri } = parseResult.data;
+          const { code, code_verifier, redirect_uri, resource } = parseResult.data;
 
           const skipLocalPkceValidation = provider.skipLocalPkceValidation;
 
@@ -107,7 +108,8 @@ export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHand
             client, 
             code, 
             skipLocalPkceValidation ? code_verifier : undefined,
-            redirect_uri
+            redirect_uri,
+            resource ? new URL(resource) : undefined
           );
           res.status(200).json(tokens);
           break;
@@ -119,10 +121,10 @@ export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHand
             throw new InvalidRequestError(parseResult.error.message);
           }
 
-          const { refresh_token, scope } = parseResult.data;
+          const { refresh_token, scope, resource } = parseResult.data;
 
           const scopes = scope?.split(" ");
-          const tokens = await provider.exchangeRefreshToken(client, refresh_token, scopes);
+          const tokens = await provider.exchangeRefreshToken(client, refresh_token, scopes, resource ? new URL(resource) : undefined);
           res.status(200).json(tokens);
           break;
         }
@@ -140,7 +142,6 @@ export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHand
         const status = error instanceof ServerError ? 500 : 400;
         res.status(status).json(error.toResponseObject());
       } else {
-        console.error("Unexpected error exchanging token:", error);
         const serverError = new ServerError("Internal Server Error");
         res.status(500).json(serverError.toResponseObject());
       }
diff --git a/src/server/auth/middleware/bearerAuth.ts b/src/server/auth/middleware/bearerAuth.ts
index fd96055..91f763a 100644
--- a/src/server/auth/middleware/bearerAuth.ts
+++ b/src/server/auth/middleware/bearerAuth.ts
@@ -88,7 +88,6 @@ export function requireBearerAuth({ verifier, requiredScopes = [], resourceMetad
       } else if (error instanceof OAuthError) {
         res.status(400).json(error.toResponseObject());
       } else {
-        console.error("Unexpected error authenticating bearer token:", error);
         const serverError = new ServerError("Internal Server Error");
         res.status(500).json(serverError.toResponseObject());
       }
diff --git a/src/server/auth/middleware/clientAuth.ts b/src/server/auth/middleware/clientAuth.ts
index 76049c1..ecd9a7b 100644
--- a/src/server/auth/middleware/clientAuth.ts
+++ b/src/server/auth/middleware/clientAuth.ts
@@ -64,7 +64,6 @@ export function authenticateClient({ clientsStore }: ClientAuthenticationMiddlew
         const status = error instanceof ServerError ? 500 : 400;
         res.status(status).json(error.toResponseObject());
       } else {
-        console.error("Unexpected error authenticating client:", error);
         const serverError = new ServerError("Internal Server Error");
         res.status(500).json(serverError.toResponseObject());
       }
diff --git a/src/server/auth/provider.ts b/src/server/auth/provider.ts
index 7815b71..18beb21 100644
--- a/src/server/auth/provider.ts
+++ b/src/server/auth/provider.ts
@@ -8,6 +8,7 @@ export type AuthorizationParams = {
   scopes?: string[];
   codeChallenge: string;
   redirectUri: string;
+  resource?: URL;
 };
 
 /**
@@ -40,13 +41,14 @@ export interface OAuthServerProvider {
     client: OAuthClientInformationFull, 
     authorizationCode: string, 
     codeVerifier?: string,
-    redirectUri?: string
+    redirectUri?: string,
+    resource?: URL
   ): Promise<OAuthTokens>;
 
   /**
    * Exchanges a refresh token for an access token.
    */
-  exchangeRefreshToken(client: OAuthClientInformationFull, refreshToken: string, scopes?: string[]): Promise<OAuthTokens>;
+  exchangeRefreshToken(client: OAuthClientInformationFull, refreshToken: string, scopes?: string[], resource?: URL): Promise<OAuthTokens>;
 
   /**
    * Verifies an access token and returns information about it.
diff --git a/src/server/auth/providers/proxyProvider.test.ts b/src/server/auth/providers/proxyProvider.test.ts
index 69039c3..4e98d0d 100644
--- a/src/server/auth/providers/proxyProvider.test.ts
+++ b/src/server/auth/providers/proxyProvider.test.ts
@@ -88,6 +88,7 @@ describe("Proxy OAuth Server Provider", () => {
           codeChallenge: "test-challenge",
           state: "test-state",
           scopes: ["read", "write"],
+          resource: new URL('https://api.example.com/resource'),
         },
         mockResponse
       );
@@ -100,6 +101,7 @@ describe("Proxy OAuth Server Provider", () => {
       expectedUrl.searchParams.set("code_challenge_method", "S256");
       expectedUrl.searchParams.set("state", "test-state");
       expectedUrl.searchParams.set("scope", "read write");
+      expectedUrl.searchParams.set('resource', 'https://api.example.com/resource');
 
       expect(mockResponse.redirect).toHaveBeenCalledWith(expectedUrl.toString());
     });
@@ -164,6 +166,41 @@ describe("Proxy OAuth Server Provider", () => {
       expect(tokens).toEqual(mockTokenResponse);
     });
 
+    it('includes resource parameter in authorization code exchange', async () => {
+      const tokens = await provider.exchangeAuthorizationCode(
+        validClient,
+        'test-code',
+        'test-verifier',
+        'https://example.com/callback',
+        new URL('https://api.example.com/resource')
+      );
+
+      expect(global.fetch).toHaveBeenCalledWith(
+        'https://auth.example.com/token',
+        expect.objectContaining({
+          method: 'POST',
+          headers: {
+            'Content-Type': 'application/x-www-form-urlencoded',
+          },
+          body: expect.stringContaining('resource=' + encodeURIComponent('https://api.example.com/resource'))
+        })
+      );
+      expect(tokens).toEqual(mockTokenResponse);
+    });
+
+    it('handles authorization code exchange without resource parameter', async () => {
+      const tokens = await provider.exchangeAuthorizationCode(
+        validClient,
+        'test-code',
+        'test-verifier'
+      );
+
+      const fetchCall = (global.fetch as jest.Mock).mock.calls[0];
+      const body = fetchCall[1].body as string;
+      expect(body).not.toContain('resource=');
+      expect(tokens).toEqual(mockTokenResponse);
+    });
+
     it("exchanges refresh token for new tokens", async () => {
       const tokens = await provider.exchangeRefreshToken(
         validClient,
@@ -184,6 +221,26 @@ describe("Proxy OAuth Server Provider", () => {
       expect(tokens).toEqual(mockTokenResponse);
     });
 
+    it('includes resource parameter in refresh token exchange', async () => {
+      const tokens = await provider.exchangeRefreshToken(
+        validClient,
+        'test-refresh-token',
+        ['read', 'write'],
+        new URL('https://api.example.com/resource')
+      );
+
+      expect(global.fetch).toHaveBeenCalledWith(
+        'https://auth.example.com/token',
+        expect.objectContaining({
+          method: 'POST',
+          headers: {
+            'Content-Type': 'application/x-www-form-urlencoded',
+          },
+          body: expect.stringContaining('resource=' + encodeURIComponent('https://api.example.com/resource'))
+        })
+      );
+      expect(tokens).toEqual(mockTokenResponse);
+    });
   });
 
   describe("client registration", () => {
diff --git a/src/server/auth/providers/proxyProvider.ts b/src/server/auth/providers/proxyProvider.ts
index db7460e..de74862 100644
--- a/src/server/auth/providers/proxyProvider.ts
+++ b/src/server/auth/providers/proxyProvider.ts
@@ -134,6 +134,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
     // Add optional standard OAuth parameters
     if (params.state) searchParams.set("state", params.state);
     if (params.scopes?.length) searchParams.set("scope", params.scopes.join(" "));
+    if (params.resource) searchParams.set("resource", params.resource.href);
 
     targetUrl.search = searchParams.toString();
     res.redirect(targetUrl.toString());
@@ -152,7 +153,8 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
     client: OAuthClientInformationFull,
     authorizationCode: string,
     codeVerifier?: string,
-    redirectUri?: string
+    redirectUri?: string,
+    resource?: URL
   ): Promise<OAuthTokens> {
     const params = new URLSearchParams({
       grant_type: "authorization_code",
@@ -172,6 +174,10 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
       params.append("redirect_uri", redirectUri);
     }
 
+    if (resource) {
+      params.append("resource", resource.href);
+    }
+
     const response = await fetch(this._endpoints.tokenUrl, {
       method: "POST",
       headers: {
@@ -192,7 +198,8 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
   async exchangeRefreshToken(
     client: OAuthClientInformationFull,
     refreshToken: string,
-    scopes?: string[]
+    scopes?: string[],
+    resource?: URL
   ): Promise<OAuthTokens> {
 
     const params = new URLSearchParams({
@@ -209,6 +216,10 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
       params.set("scope", scopes.join(" "));
     }
 
+    if (resource) {
+      params.set("resource", resource.href);
+    }
+
     const response = await fetch(this._endpoints.tokenUrl, {
       method: "POST",
       headers: {
diff --git a/src/server/auth/types.ts b/src/server/auth/types.ts
index c25c2b6..0189e9e 100644
--- a/src/server/auth/types.ts
+++ b/src/server/auth/types.ts
@@ -22,6 +22,12 @@ export interface AuthInfo {
    */
   expiresAt?: number;
 
+  /**
+   * The RFC 8707 resource server identifier for which this token is valid.
+   * If set, this MUST match the MCP server's resource identifier (minus hash fragment).
+   */
+  resource?: URL;
+
   /**
    * Additional data associated with the token.
    * This field should be used for any additional data that needs to be attached to the auth info.
diff --git a/src/server/completable.ts b/src/server/completable.ts
index 3b5bc16..652eaf7 100644
--- a/src/server/completable.ts
+++ b/src/server/completable.ts
@@ -15,6 +15,9 @@ export enum McpZodTypeKind {
 
 export type CompleteCallback<T extends ZodTypeAny = ZodTypeAny> = (
   value: T["_input"],
+  context?: {
+    arguments?: Record<string, string>;
+  },
 ) => T["_input"][] | Promise<T["_input"][]>;
 
 export interface CompletableDef<T extends ZodTypeAny = ZodTypeAny>
diff --git a/src/server/index.test.ts b/src/server/index.test.ts
index 7c0fbc5..d91b90a 100644
--- a/src/server/index.test.ts
+++ b/src/server/index.test.ts
@@ -10,11 +10,12 @@ import {
   LATEST_PROTOCOL_VERSION,
   SUPPORTED_PROTOCOL_VERSIONS,
   CreateMessageRequestSchema,
+  ElicitRequestSchema,
   ListPromptsRequestSchema,
   ListResourcesRequestSchema,
   ListToolsRequestSchema,
   SetLevelRequestSchema,
-  ErrorCode,
+  ErrorCode
 } from "../types.js";
 import { Transport } from "../shared/transport.js";
 import { InMemoryTransport } from "../inMemory.js";
@@ -267,6 +268,318 @@ test("should respect client capabilities", async () => {
   await expect(server.listRoots()).rejects.toThrow(/^Client does not support/);
 });
 
+test("should respect client elicitation capabilities", async () => {
+  const server = new Server(
+    {
+      name: "test server",
+      version: "1.0",
+    },
+    {
+      capabilities: {
+        prompts: {},
+        resources: {},
+        tools: {},
+        logging: {},
+      },
+      enforceStrictCapabilities: true,
+    },
+  );
+
+  const client = new Client(
+    {
+      name: "test client",
+      version: "1.0",
+    },
+    {
+      capabilities: {
+        elicitation: {},
+      },
+    },
+  );
+
+  client.setRequestHandler(ElicitRequestSchema, (params) => ({
+    action: "accept",
+    content: {
+      username: params.params.message.includes("username") ? "test-user" : undefined,
+      confirmed: true,
+    },
+  }));
+
+  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+  await Promise.all([
+    client.connect(clientTransport),
+    server.connect(serverTransport),
+  ]);
+
+  expect(server.getClientCapabilities()).toEqual({ elicitation: {} });
+
+  // This should work because elicitation is supported by the client
+  await expect(
+    server.elicitInput({
+      message: "Please provide your username",
+      requestedSchema: {
+        type: "object",
+        properties: {
+          username: {
+            type: "string",
+            title: "Username",
+            description: "Your username",
+          },
+          confirmed: {
+            type: "boolean",
+            title: "Confirm",
+            description: "Please confirm",
+            default: false,
+          },
+        },
+        required: ["username"],
+      },
+    }),
+  ).resolves.toEqual({
+    action: "accept",
+    content: {
+      username: "test-user",
+      confirmed: true,
+    },
+  });
+
+  // This should still throw because sampling is not supported by the client
+  await expect(
+    server.createMessage({
+      messages: [],
+      maxTokens: 10,
+    }),
+  ).rejects.toThrow(/^Client does not support/);
+});
+
+test("should validate elicitation response against requested schema", async () => {
+  const server = new Server(
+    {
+      name: "test server",
+      version: "1.0",
+    },
+    {
+      capabilities: {
+        prompts: {},
+        resources: {},
+        tools: {},
+        logging: {},
+      },
+      enforceStrictCapabilities: true,
+    },
+  );
+
+  const client = new Client(
+    {
+      name: "test client",
+      version: "1.0",
+    },
+    {
+      capabilities: {
+        elicitation: {},
+      },
+    },
+  );
+
+  // Set up client to return valid response
+  client.setRequestHandler(ElicitRequestSchema, (request) => ({
+    action: "accept",
+    content: {
+      name: "John Doe",
+      email: "john@example.com",
+      age: 30,
+    },
+  }));
+
+  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+  await Promise.all([
+    client.connect(clientTransport),
+    server.connect(serverTransport),
+  ]);
+
+  // Test with valid response
+  await expect(
+    server.elicitInput({
+      message: "Please provide your information",
+      requestedSchema: {
+        type: "object",
+        properties: {
+          name: {
+            type: "string",
+            minLength: 1,
+          },
+          email: {
+            type: "string",
+            minLength: 1,
+          },
+          age: {
+            type: "integer",
+            minimum: 0,
+            maximum: 150,
+          },
+        },
+        required: ["name", "email"],
+      },
+    }),
+  ).resolves.toEqual({
+    action: "accept",
+    content: {
+      name: "John Doe",
+      email: "john@example.com",
+      age: 30,
+    },
+  });
+});
+
+test("should reject elicitation response with invalid data", async () => {
+  const server = new Server(
+    {
+      name: "test server",
+      version: "1.0",
+    },
+    {
+      capabilities: {
+        prompts: {},
+        resources: {},
+        tools: {},
+        logging: {},
+      },
+      enforceStrictCapabilities: true,
+    },
+  );
+
+  const client = new Client(
+    {
+      name: "test client",
+      version: "1.0",
+    },
+    {
+      capabilities: {
+        elicitation: {},
+      },
+    },
+  );
+
+  // Set up client to return invalid response (missing required field, invalid age)
+  client.setRequestHandler(ElicitRequestSchema, (request) => ({
+    action: "accept",
+    content: {
+      email: "", // Invalid - too short
+      age: -5, // Invalid age
+    },
+  }));
+
+  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+  await Promise.all([
+    client.connect(clientTransport),
+    server.connect(serverTransport),
+  ]);
+
+  // Test with invalid response
+  await expect(
+    server.elicitInput({
+      message: "Please provide your information",
+      requestedSchema: {
+        type: "object",
+        properties: {
+          name: {
+            type: "string",
+            minLength: 1,
+          },
+          email: {
+            type: "string",
+            minLength: 1,
+          },
+          age: {
+            type: "integer",
+            minimum: 0,
+            maximum: 150,
+          },
+        },
+        required: ["name", "email"],
+      },
+    }),
+  ).rejects.toThrow(/does not match requested schema/);
+});
+
+test("should allow elicitation reject and cancel without validation", async () => {
+  const server = new Server(
+    {
+      name: "test server",
+      version: "1.0",
+    },
+    {
+      capabilities: {
+        prompts: {},
+        resources: {},
+        tools: {},
+        logging: {},
+      },
+      enforceStrictCapabilities: true,
+    },
+  );
+
+  const client = new Client(
+    {
+      name: "test client",
+      version: "1.0",
+    },
+    {
+      capabilities: {
+        elicitation: {},
+      },
+    },
+  );
+
+  let requestCount = 0;
+  client.setRequestHandler(ElicitRequestSchema, (request) => {
+    requestCount++;
+    if (requestCount === 1) {
+      return { action: "reject" };
+    } else {
+      return { action: "cancel" };
+    }
+  });
+
+  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+  await Promise.all([
+    client.connect(clientTransport),
+    server.connect(serverTransport),
+  ]);
+
+  const schema = {
+    type: "object" as const,
+    properties: {
+      name: { type: "string" as const },
+    },
+    required: ["name"],
+  };
+
+  // Test reject - should not validate
+  await expect(
+    server.elicitInput({
+      message: "Please provide your name",
+      requestedSchema: schema,
+    }),
+  ).resolves.toEqual({
+    action: "reject",
+  });
+
+  // Test cancel - should not validate  
+  await expect(
+    server.elicitInput({
+      message: "Please provide your name",
+      requestedSchema: schema,
+    }),
+  ).resolves.toEqual({
+    action: "cancel",
+  });
+});
+
 test("should respect server notification capabilities", async () => {
   const server = new Server(
     {
diff --git a/src/server/index.ts b/src/server/index.ts
index 3901099..10ae2fa 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -8,6 +8,9 @@ import {
   ClientCapabilities,
   CreateMessageRequest,
   CreateMessageResultSchema,
+  ElicitRequest,
+  ElicitResult,
+  ElicitResultSchema,
   EmptyResultSchema,
   Implementation,
   InitializedNotificationSchema,
@@ -18,6 +21,8 @@ import {
   ListRootsRequest,
   ListRootsResultSchema,
   LoggingMessageNotification,
+  McpError,
+  ErrorCode,
   Notification,
   Request,
   ResourceUpdatedNotification,
@@ -28,6 +33,7 @@ import {
   ServerResult,
   SUPPORTED_PROTOCOL_VERSIONS,
 } from "../types.js";
+import Ajv from "ajv";
 
 export type ServerOptions = ProtocolOptions & {
   /**
@@ -129,6 +135,14 @@ export class Server<
         }
         break;
 
+      case "elicitation/create":
+        if (!this._clientCapabilities?.elicitation) {
+          throw new Error(
+            `Client does not support elicitation (required for ${method})`,
+          );
+        }
+        break;
+
       case "roots/list":
         if (!this._clientCapabilities?.roots) {
           throw new Error(
@@ -251,10 +265,12 @@ export class Server<
     this._clientCapabilities = request.params.capabilities;
     this._clientVersion = request.params.clientInfo;
 
-    return {
-      protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)
+    const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)
         ? requestedVersion
-        : LATEST_PROTOCOL_VERSION,
+        : LATEST_PROTOCOL_VERSION;
+
+    return {
+      protocolVersion,
       capabilities: this.getCapabilities(),
       serverInfo: this._serverInfo,
       ...(this._instructions && { instructions: this._instructions }),
@@ -294,6 +310,44 @@ export class Server<
     );
   }
 
+  async elicitInput(
+    params: ElicitRequest["params"],
+    options?: RequestOptions,
+  ): Promise<ElicitResult> {
+    const result = await this.request(
+      { method: "elicitation/create", params },
+      ElicitResultSchema,
+      options,
+    );
+
+    // Validate the response content against the requested schema if action is "accept"
+    if (result.action === "accept" && result.content) {
+      try {
+        const ajv = new Ajv();
+        
+        const validate = ajv.compile(params.requestedSchema);
+        const isValid = validate(result.content);
+        
+        if (!isValid) {
+          throw new McpError(
+            ErrorCode.InvalidParams,
+            `Elicitation response content does not match requested schema: ${ajv.errorsText(validate.errors)}`,
+          );
+        }
+      } catch (error) {
+        if (error instanceof McpError) {
+          throw error;
+        }
+        throw new McpError(
+          ErrorCode.InternalError,
+          `Error validating elicitation response: ${error}`,
+        );
+      }
+    }
+
+    return result;
+  }
+
   async listRoots(
     params?: ListRootsRequest["params"],
     options?: RequestOptions,
diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts
index 0ba1998..0764ffe 100644
--- a/src/server/mcp.test.ts
+++ b/src/server/mcp.test.ts
@@ -14,10 +14,12 @@ import {
   LoggingMessageNotificationSchema,
   Notification,
   TextContent,
+  ElicitRequestSchema
 } from "../types.js";
 import { ResourceTemplate } from "./mcp.js";
 import { completable } from "./completable.js";
 import { UriTemplate } from "../shared/uriTemplate.js";
+import { getDisplayName } from "../shared/metadataUtils.js";
 
 describe("McpServer", () => {
   /***
@@ -1201,6 +1203,68 @@ describe("tool()", () => {
       }),
     ).rejects.toThrow(/Tool test has an output schema but no structured content was provided/);
   });
+  /***
+   * Test: Tool with Output Schema Must Provide Structured Content
+   */
+  test("should skip outputSchema validation when isError is true", async () => {
+    const mcpServer = new McpServer({
+      name: "test server",
+      version: "1.0",
+    });
+
+    const client = new Client({
+      name: "test client",
+      version: "1.0",
+    });
+
+    mcpServer.registerTool(
+      "test",
+      {
+        description: "Test tool with output schema but missing structured content",
+        inputSchema: {
+          input: z.string(),
+        },
+        outputSchema: {
+          processedInput: z.string(),
+          resultType: z.string(),
+        },
+      },
+      async ({ input }) => ({
+        content: [
+          {
+            type: "text",
+            text: `Processed: ${input}`,
+          },
+        ],
+        isError: true,
+      })
+    );
+
+    const [clientTransport, serverTransport] =
+      InMemoryTransport.createLinkedPair();
+
+    await Promise.all([
+      client.connect(clientTransport),
+      mcpServer.server.connect(serverTransport),
+    ]);
+
+    await expect(
+      client.callTool({
+        name: "test",
+        arguments: {
+          input: "hello",
+        },
+      }),
+    ).resolves.toStrictEqual({
+      content: [
+        {
+          type: "text",
+          text: `Processed: hello`,
+        },
+      ],
+      isError: true,
+    });
+  });
 
   /***
    * Test: Schema Validation Failure for Invalid Structured Content
@@ -3457,4 +3521,772 @@ describe("prompt()", () => {
     expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true);
     expect(result.messages[0].content.text).toContain("Received request ID:");
   });
+
+  /***
+   * Test: Resource Template Metadata Priority
+   */
+  test("should prioritize individual resource metadata over template metadata", async () => {
+    const mcpServer = new McpServer({
+      name: "test server",
+      version: "1.0",
+    });
+    const client = new Client({
+      name: "test client",
+      version: "1.0",
+    });
+
+    mcpServer.resource(
+      "test",
+      new ResourceTemplate("test://resource/{id}", {
+        list: async () => ({
+          resources: [
+            {
+              name: "Resource 1",
+              uri: "test://resource/1",
+              description: "Individual resource description",
+              mimeType: "text/plain",
+            },
+            {
+              name: "Resource 2",
+              uri: "test://resource/2",
+              // This resource has no description or mimeType
+            },
+          ],
+        }),
+      }),
+      {
+        description: "Template description",
+        mimeType: "application/json",
+      },
+      async (uri) => ({
+        contents: [
+          {
+            uri: uri.href,
+            text: "Test content",
+          },
+        ],
+      }),
+    );
+
+    const [clientTransport, serverTransport] =
+      InMemoryTransport.createLinkedPair();
+
+    await Promise.all([
+      client.connect(clientTransport),
+      mcpServer.server.connect(serverTransport),
+    ]);
+
+    const result = await client.request(
+      {
+        method: "resources/list",
+      },
+      ListResourcesResultSchema,
+    );
+
+    expect(result.resources).toHaveLength(2);
+
+    // Resource 1 should have its own metadata
+    expect(result.resources[0].name).toBe("Resource 1");
+    expect(result.resources[0].description).toBe("Individual resource description");
+    expect(result.resources[0].mimeType).toBe("text/plain");
+
+    // Resource 2 should inherit template metadata
+    expect(result.resources[1].name).toBe("Resource 2");
+    expect(result.resources[1].description).toBe("Template description");
+    expect(result.resources[1].mimeType).toBe("application/json");
+  });
+
+  /***
+   * Test: Resource Template Metadata Overrides All Fields
+   */
+  test("should allow resource to override all template metadata fields", async () => {
+    const mcpServer = new McpServer({
+      name: "test server",
+      version: "1.0",
+    });
+    const client = new Client({
+      name: "test client",
+      version: "1.0",
+    });
+
+    mcpServer.resource(
+      "test",
+      new ResourceTemplate("test://resource/{id}", {
+        list: async () => ({
+          resources: [
+            {
+              name: "Overridden Name",
+              uri: "test://resource/1",
+              description: "Overridden description",
+              mimeType: "text/markdown",
+              // Add any other metadata fields if they exist
+            },
+          ],
+        }),
+      }),
+      {
+        name: "Template Name",
+        description: "Template description",
+        mimeType: "application/json",
+      },
+      async (uri) => ({
+        contents: [
+          {
+            uri: uri.href,
+            text: "Test content",
+          },
+        ],
+      }),
+    );
+
+    const [clientTransport, serverTransport] =
+      InMemoryTransport.createLinkedPair();
+
+    await Promise.all([
+      client.connect(clientTransport),
+      mcpServer.server.connect(serverTransport),
+    ]);
+
+    const result = await client.request(
+      {
+        method: "resources/list",
+      },
+      ListResourcesResultSchema,
+    );
+
+    expect(result.resources).toHaveLength(1);
+
+    // All fields should be from the individual resource, not the template
+    expect(result.resources[0].name).toBe("Overridden Name");
+    expect(result.resources[0].description).toBe("Overridden description");
+    expect(result.resources[0].mimeType).toBe("text/markdown");
+  });
+});
+
+describe("Tool title precedence", () => {
+  test("should follow correct title precedence: title → annotations.title → name", async () => {
+    const mcpServer = new McpServer({
+      name: "test server",
+      version: "1.0",
+    });
+    const client = new Client({
+      name: "test client",
+      version: "1.0",
+    });
+
+    // Tool 1: Only name
+    mcpServer.tool(
+      "tool_name_only",
+      async () => ({
+        content: [{ type: "text", text: "Response" }],
+      })
+    );
+
+    // Tool 2: Name and annotations.title
+    mcpServer.tool(
+      "tool_with_annotations_title",
+      "Tool with annotations title",
+      {
+        title: "Annotations Title"
+      },
+      async () => ({
+        content: [{ type: "text", text: "Response" }],
+      })
+    );
+
+    // Tool 3: Name and title (using registerTool)
+    mcpServer.registerTool(
+      "tool_with_title",
+      {
+        title: "Regular Title",
+        description: "Tool with regular title"
+      },
+      async () => ({
+        content: [{ type: "text", text: "Response" }],
+      })
+    );
+
+    // Tool 4: All three - title should win
+    mcpServer.registerTool(
+      "tool_with_all_titles",
+      {
+        title: "Regular Title Wins",
+        description: "Tool with all titles",
+        annotations: {
+          title: "Annotations Title Should Not Show"
+        }
+      },
+      async () => ({
+        content: [{ type: "text", text: "Response" }],
+      })
+    );
+
+    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+    await Promise.all([
+      client.connect(clientTransport),
+      mcpServer.connect(serverTransport),
+    ]);
+
+    const result = await client.request(
+      { method: "tools/list" },
+      ListToolsResultSchema,
+    );
+
+
+    expect(result.tools).toHaveLength(4);
+
+    // Tool 1: Only name - should display name
+    const tool1 = result.tools.find(t => t.name === "tool_name_only");
+    expect(tool1).toBeDefined();
+    expect(getDisplayName(tool1!)).toBe("tool_name_only");
+
+    // Tool 2: Name and annotations.title - should display annotations.title
+    const tool2 = result.tools.find(t => t.name === "tool_with_annotations_title");
+    expect(tool2).toBeDefined();
+    expect(tool2!.annotations?.title).toBe("Annotations Title");
+    expect(getDisplayName(tool2!)).toBe("Annotations Title");
+
+    // Tool 3: Name and title - should display title
+    const tool3 = result.tools.find(t => t.name === "tool_with_title");
+    expect(tool3).toBeDefined();
+    expect(tool3!.title).toBe("Regular Title");
+    expect(getDisplayName(tool3!)).toBe("Regular Title");
+
+    // Tool 4: All three - title should take precedence
+    const tool4 = result.tools.find(t => t.name === "tool_with_all_titles");
+    expect(tool4).toBeDefined();
+    expect(tool4!.title).toBe("Regular Title Wins");
+    expect(tool4!.annotations?.title).toBe("Annotations Title Should Not Show");
+    expect(getDisplayName(tool4!)).toBe("Regular Title Wins");
+  });
+
+  test("getDisplayName unit tests for title precedence", () => {
+
+    // Test 1: Only name
+    expect(getDisplayName({ name: "tool_name" })).toBe("tool_name");
+
+    // Test 2: Name and title - title wins
+    expect(getDisplayName({
+      name: "tool_name",
+      title: "Tool Title"
+    })).toBe("Tool Title");
+
+    // Test 3: Name and annotations.title - annotations.title wins
+    expect(getDisplayName({
+      name: "tool_name",
+      annotations: { title: "Annotations Title" }
+    })).toBe("Annotations Title");
+
+    // Test 4: All three - title wins (correct precedence)
+    expect(getDisplayName({
+      name: "tool_name",
+      title: "Regular Title",
+      annotations: { title: "Annotations Title" }
+    })).toBe("Regular Title");
+
+    // Test 5: Empty title should not be used
+    expect(getDisplayName({
+      name: "tool_name",
+      title: "",
+      annotations: { title: "Annotations Title" }
+    })).toBe("Annotations Title");
+
+    // Test 6: Undefined vs null handling
+    expect(getDisplayName({
+      name: "tool_name",
+      title: undefined,
+      annotations: { title: "Annotations Title" }
+    })).toBe("Annotations Title");
+  });
+
+  test("should support resource template completion with resolved context", async () => {
+    const mcpServer = new McpServer({
+      name: "test server",
+      version: "1.0",
+    });
+
+    const client = new Client({
+      name: "test client",
+      version: "1.0",
+    });
+
+    mcpServer.registerResource(
+      "test",
+      new ResourceTemplate("github://repos/{owner}/{repo}", {
+        list: undefined,
+        complete: {
+          repo: (value, context) => {
+            if (context?.arguments?.["owner"] === "org1") {
+              return ["project1", "project2", "project3"].filter(r => r.startsWith(value));
+            } else if (context?.arguments?.["owner"] === "org2") {
+              return ["repo1", "repo2", "repo3"].filter(r => r.startsWith(value));
+            }
+            return [];
+          },
+        },
+      }),
+      {
+        title: "GitHub Repository",
+        description: "Repository information"
+      },
+      async () => ({
+        contents: [
+          {
+            uri: "github://repos/test/test",
+            text: "Test content",
+          },
+        ],
+      }),
+    );
+
+    const [clientTransport, serverTransport] =
+      InMemoryTransport.createLinkedPair();
+
+    await Promise.all([
+      client.connect(clientTransport),
+      mcpServer.server.connect(serverTransport),
+    ]);
+
+    // Test with microsoft owner
+    const result1 = await client.request(
+      {
+        method: "completion/complete",
+        params: {
+          ref: {
+            type: "ref/resource",
+            uri: "github://repos/{owner}/{repo}",
+          },
+          argument: {
+            name: "repo",
+            value: "p",
+          },
+          context: {
+            arguments: {
+              owner: "org1",
+            },
+          },
+        },
+      },
+      CompleteResultSchema,
+    );
+
+    expect(result1.completion.values).toEqual(["project1", "project2", "project3"]);
+    expect(result1.completion.total).toBe(3);
+
+    // Test with facebook owner
+    const result2 = await client.request(
+      {
+        method: "completion/complete",
+        params: {
+          ref: {
+            type: "ref/resource",
+            uri: "github://repos/{owner}/{repo}",
+          },
+          argument: {
+            name: "repo",
+            value: "r",
+          },
+          context: {
+            arguments: {
+              owner: "org2",
+            },
+          },
+        },
+      },
+      CompleteResultSchema,
+    );
+
+    expect(result2.completion.values).toEqual(["repo1", "repo2", "repo3"]);
+    expect(result2.completion.total).toBe(3);
+
+    // Test with no resolved context
+    const result3 = await client.request(
+      {
+        method: "completion/complete",
+        params: {
+          ref: {
+            type: "ref/resource",
+            uri: "github://repos/{owner}/{repo}",
+          },
+          argument: {
+            name: "repo",
+            value: "t",
+          },
+        },
+      },
+      CompleteResultSchema,
+    );
+
+    expect(result3.completion.values).toEqual([]);
+    expect(result3.completion.total).toBe(0);
+  });
+
+  test("should support prompt argument completion with resolved context", async () => {
+    const mcpServer = new McpServer({
+      name: "test server",
+      version: "1.0",
+    });
+
+    const client = new Client({
+      name: "test client",
+      version: "1.0",
+    });
+
+    mcpServer.registerPrompt(
+      "test-prompt",
+      {
+        title: "Team Greeting",
+        description: "Generate a greeting for team members",
+        argsSchema: {
+          department: completable(z.string(), (value) => {
+            return ["engineering", "sales", "marketing", "support"].filter(d => d.startsWith(value));
+          }),
+          name: completable(z.string(), (value, context) => {
+            const department = context?.arguments?.["department"];
+            if (department === "engineering") {
+              return ["Alice", "Bob", "Charlie"].filter(n => n.startsWith(value));
+            } else if (department === "sales") {
+              return ["David", "Eve", "Frank"].filter(n => n.startsWith(value));
+            } else if (department === "marketing") {
+              return ["Grace", "Henry", "Iris"].filter(n => n.startsWith(value));
+            }
+            return ["Guest"].filter(n => n.startsWith(value));
+          }),
+        }
+      },
+      async ({ department, name }) => ({
+        messages: [
+          {
+            role: "assistant",
+            content: {
+              type: "text",
+              text: `Hello ${name}, welcome to the ${department} team!`,
+            },
+          },
+        ],
+      }),
+    );
+
+    const [clientTransport, serverTransport] =
+      InMemoryTransport.createLinkedPair();
+
+    await Promise.all([
+      client.connect(clientTransport),
+      mcpServer.server.connect(serverTransport),
+    ]);
+
+    // Test with engineering department
+    const result1 = await client.request(
+      {
+        method: "completion/complete",
+        params: {
+          ref: {
+            type: "ref/prompt",
+            name: "test-prompt",
+          },
+          argument: {
+            name: "name",
+            value: "A",
+          },
+          context: {
+            arguments: {
+              department: "engineering",
+            },
+          },
+        },
+      },
+      CompleteResultSchema,
+    );
+
+    expect(result1.completion.values).toEqual(["Alice"]);
+
+    // Test with sales department
+    const result2 = await client.request(
+      {
+        method: "completion/complete",
+        params: {
+          ref: {
+            type: "ref/prompt",
+            name: "test-prompt",
+          },
+          argument: {
+            name: "name",
+            value: "D",
+          },
+          context: {
+            arguments: {
+              department: "sales",
+            },
+          },
+        },
+      },
+      CompleteResultSchema,
+    );
+
+    expect(result2.completion.values).toEqual(["David"]);
+
+    // Test with marketing department
+    const result3 = await client.request(
+      {
+        method: "completion/complete",
+        params: {
+          ref: {
+            type: "ref/prompt",
+            name: "test-prompt",
+          },
+          argument: {
+            name: "name",
+            value: "G",
+          },
+          context: {
+            arguments: {
+              department: "marketing",
+            },
+          },
+        },
+      },
+      CompleteResultSchema,
+    );
+
+    expect(result3.completion.values).toEqual(["Grace"]);
+
+    // Test with no resolved context
+    const result4 = await client.request(
+      {
+        method: "completion/complete",
+        params: {
+          ref: {
+            type: "ref/prompt",
+            name: "test-prompt",
+          },
+          argument: {
+            name: "name",
+            value: "G",
+          },
+        },
+      },
+      CompleteResultSchema,
+    );
+
+    expect(result4.completion.values).toEqual(["Guest"]);
+  });
+});
+
+describe("elicitInput()", () => {
+
+  const checkAvailability = jest.fn().mockResolvedValue(false);
+  const findAlternatives = jest.fn().mockResolvedValue([]);
+  const makeBooking = jest.fn().mockResolvedValue("BOOKING-123");
+
+  let mcpServer: McpServer;
+  let client: Client;
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+
+    // Create server with restaurant booking tool
+    mcpServer = new McpServer({
+      name: "restaurant-booking-server",
+      version: "1.0.0",
+    });
+
+    // Register the restaurant booking tool from README example
+    mcpServer.tool(
+      "book-restaurant",
+      {
+        restaurant: z.string(),
+        date: z.string(),
+        partySize: z.number()
+      },
+      async ({ restaurant, date, partySize }) => {
+        // Check availability
+        const available = await checkAvailability(restaurant, date, partySize);
+
+        if (!available) {
+          // Ask user if they want to try alternative dates
+          const result = await mcpServer.server.elicitInput({
+            message: `No tables available at ${restaurant} on ${date}. Would you like to check alternative dates?`,
+            requestedSchema: {
+              type: "object",
+              properties: {
+                checkAlternatives: {
+                  type: "boolean",
+                  title: "Check alternative dates",
+                  description: "Would you like me to check other dates?"
+                },
+                flexibleDates: {
+                  type: "string",
+                  title: "Date flexibility",
+                  description: "How flexible are your dates?",
+                  enum: ["next_day", "same_week", "next_week"],
+                  enumNames: ["Next day", "Same week", "Next week"]
+                }
+              },
+              required: ["checkAlternatives"]
+            }
+          });
+
+          if (result.action === "accept" && result.content?.checkAlternatives) {
+            const alternatives = await findAlternatives(
+              restaurant,
+              date,
+              partySize,
+              result.content.flexibleDates as string
+            );
+            return {
+              content: [{
+                type: "text",
+                text: `Found these alternatives: ${alternatives.join(", ")}`
+              }]
+            };
+          }
+
+          return {
+            content: [{
+              type: "text",
+              text: "No booking made. Original date not available."
+            }]
+          };
+        }
+
+        await makeBooking(restaurant, date, partySize);
+        return {
+          content: [{
+            type: "text",
+            text: `Booked table for ${partySize} at ${restaurant} on ${date}`
+          }]
+        };
+      }
+    );
+
+    // Create client with elicitation capability
+    client = new Client(
+      {
+        name: "test-client",
+        version: "1.0.0",
+      },
+      {
+        capabilities: {
+          elicitation: {},
+        },
+      }
+    );
+  });
+
+  test("should successfully elicit additional information", async () => {
+    // Mock availability check to return false
+    checkAvailability.mockResolvedValue(false);
+    findAlternatives.mockResolvedValue(["2024-12-26", "2024-12-27", "2024-12-28"]);
+
+    // Set up client to accept alternative date checking
+    client.setRequestHandler(ElicitRequestSchema, async (request) => {
+      expect(request.params.message).toContain("No tables available at ABC Restaurant on 2024-12-25");
+      return {
+        action: "accept",
+        content: {
+          checkAlternatives: true,
+          flexibleDates: "same_week"
+        }
+      };
+    });
+
+    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+    await Promise.all([
+      client.connect(clientTransport),
+      mcpServer.server.connect(serverTransport),
+    ]);
+
+    // Call the tool
+    const result = await client.callTool({
+      name: "book-restaurant",
+      arguments: {
+        restaurant: "ABC Restaurant",
+        date: "2024-12-25",
+        partySize: 2
+      }
+    });
+
+    expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
+    expect(findAlternatives).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2, "same_week");
+    expect(result.content).toEqual([{
+      type: "text",
+      text: "Found these alternatives: 2024-12-26, 2024-12-27, 2024-12-28"
+    }]);
+  });
+
+  test("should handle user declining to elicitation request", async () => {
+    // Mock availability check to return false
+    checkAvailability.mockResolvedValue(false);
+
+    // Set up client to reject alternative date checking
+    client.setRequestHandler(ElicitRequestSchema, async () => {
+      return {
+        action: "accept",
+        content: {
+          checkAlternatives: false
+        }
+      };
+    });
+
+    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+    await Promise.all([
+      client.connect(clientTransport),
+      mcpServer.server.connect(serverTransport),
+    ]);
+
+    // Call the tool
+    const result = await client.callTool({
+      name: "book-restaurant",
+      arguments: {
+        restaurant: "ABC Restaurant",
+        date: "2024-12-25",
+        partySize: 2
+      }
+    });
+
+    expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
+    expect(findAlternatives).not.toHaveBeenCalled();
+    expect(result.content).toEqual([{
+      type: "text",
+      text: "No booking made. Original date not available."
+    }]);
+  });
+
+  test("should handle user cancelling the elicitation", async () => {
+    // Mock availability check to return false
+    checkAvailability.mockResolvedValue(false);
+
+    // Set up client to cancel the elicitation
+    client.setRequestHandler(ElicitRequestSchema, async () => {
+      return {
+        action: "cancel"
+      };
+    });
+
+    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+    await Promise.all([
+      client.connect(clientTransport),
+      mcpServer.server.connect(serverTransport),
+    ]);
+
+    // Call the tool
+    const result = await client.callTool({
+      name: "book-restaurant",
+      arguments: {
+        restaurant: "ABC Restaurant",
+        date: "2024-12-25",
+        partySize: 2
+      }
+    });
+
+    expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
+    expect(findAlternatives).not.toHaveBeenCalled();
+    expect(result.content).toEqual([{
+      type: "text",
+      text: "No booking made. Original date not available."
+    }]);
+  });
 });
diff --git a/src/server/mcp.ts b/src/server/mcp.ts
index 38c869c..1d984ed 100644
--- a/src/server/mcp.ts
+++ b/src/server/mcp.ts
@@ -21,7 +21,8 @@ import {
   CompleteRequest,
   CompleteResult,
   PromptReference,
-  ResourceReference,
+  ResourceTemplateReference,
+  BaseMetadata,
   Resource,
   ListResourcesResult,
   ListResourceTemplatesRequestSchema,
@@ -113,6 +114,7 @@ export class McpServer {
           ([name, tool]): Tool => {
             const toolDefinition: Tool = {
               name,
+              title: tool.title,
               description: tool.description,
               inputSchema: tool.inputSchema
                 ? (zodToJsonSchema(tool.inputSchema, {
@@ -124,7 +126,7 @@ export class McpServer {
 
             if (tool.outputSchema) {
               toolDefinition.outputSchema = zodToJsonSchema(
-                tool.outputSchema, 
+                tool.outputSchema,
                 { strictUnions: true }
               ) as Tool["outputSchema"];
             }
@@ -198,7 +200,7 @@ export class McpServer {
           }
         }
 
-        if (tool.outputSchema) {
+        if (tool.outputSchema && !result.isError) {
           if (!result.structuredContent) {
             throw new McpError(
               ErrorCode.InvalidParams,
@@ -291,13 +293,13 @@ export class McpServer {
     }
 
     const def: CompletableDef<ZodString> = field._def;
-    const suggestions = await def.complete(request.params.argument.value);
+    const suggestions = await def.complete(request.params.argument.value, request.params.context);
     return createCompletionResult(suggestions);
   }
 
   private async handleResourceCompletion(
     request: CompleteRequest,
-    ref: ResourceReference,
+    ref: ResourceTemplateReference,
   ): Promise<CompleteResult> {
     const template = Object.values(this._registeredResourceTemplates).find(
       (t) => t.resourceTemplate.uriTemplate.toString() === ref.uri,
@@ -322,7 +324,7 @@ export class McpServer {
       return EMPTY_COMPLETION_RESULT;
     }
 
-    const suggestions = await completer(request.params.argument.value);
+    const suggestions = await completer(request.params.argument.value, request.params.context);
     return createCompletionResult(suggestions);
   }
 
@@ -373,8 +375,9 @@ export class McpServer {
           const result = await template.resourceTemplate.listCallback(extra);
           for (const resource of result.resources) {
             templateResources.push({
-              ...resource,
               ...template.metadata,
+              // the defined resource metadata should override the template metadata if present
+              ...resource,
             });
           }
         }
@@ -468,6 +471,7 @@ export class McpServer {
           ([name, prompt]): Prompt => {
             return {
               name,
+              title: prompt.title,
               description: prompt.description,
               arguments: prompt.argsSchema
                 ? promptArgumentsFromSchema(prompt.argsSchema)
@@ -575,27 +579,13 @@ export class McpServer {
         throw new Error(`Resource ${uriOrTemplate} is already registered`);
       }
 
-      const registeredResource: RegisteredResource = {
+      const registeredResource = this._createRegisteredResource(
         name,
+        undefined,
+        uriOrTemplate,
         metadata,
-        readCallback: readCallback as ReadResourceCallback,
-        enabled: true,
-        disable: () => registeredResource.update({ enabled: false }),
-        enable: () => registeredResource.update({ enabled: true }),
-        remove: () => registeredResource.update({ uri: null }),
-        update: (updates) => {
-          if (typeof updates.uri !== "undefined" && updates.uri !== uriOrTemplate) {
-            delete this._registeredResources[uriOrTemplate]
-            if (updates.uri) this._registeredResources[updates.uri] = registeredResource
-          }
-          if (typeof updates.name !== "undefined") registeredResource.name = updates.name
-          if (typeof updates.metadata !== "undefined") registeredResource.metadata = updates.metadata
-          if (typeof updates.callback !== "undefined") registeredResource.readCallback = updates.callback
-          if (typeof updates.enabled !== "undefined") registeredResource.enabled = updates.enabled
-          this.sendResourceListChanged()
-        },
-      };
-      this._registeredResources[uriOrTemplate] = registeredResource;
+        readCallback as ReadResourceCallback
+      );
 
       this.setResourceRequestHandlers();
       this.sendResourceListChanged();
@@ -605,27 +595,70 @@ export class McpServer {
         throw new Error(`Resource template ${name} is already registered`);
       }
 
-      const registeredResourceTemplate: RegisteredResourceTemplate = {
-        resourceTemplate: uriOrTemplate,
+      const registeredResourceTemplate = this._createRegisteredResourceTemplate(
+        name,
+        undefined,
+        uriOrTemplate,
         metadata,
-        readCallback: readCallback as ReadResourceTemplateCallback,
-        enabled: true,
-        disable: () => registeredResourceTemplate.update({ enabled: false }),
-        enable: () => registeredResourceTemplate.update({ enabled: true }),
-        remove: () => registeredResourceTemplate.update({ name: null }),
-        update: (updates) => {
-          if (typeof updates.name !== "undefined" && updates.name !== name) {
-            delete this._registeredResourceTemplates[name]
-            if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate
-          }
-          if (typeof updates.template !== "undefined") registeredResourceTemplate.resourceTemplate = updates.template
-          if (typeof updates.metadata !== "undefined") registeredResourceTemplate.metadata = updates.metadata
-          if (typeof updates.callback !== "undefined") registeredResourceTemplate.readCallback = updates.callback
-          if (typeof updates.enabled !== "undefined") registeredResourceTemplate.enabled = updates.enabled
-          this.sendResourceListChanged()
-        },
-      };
-      this._registeredResourceTemplates[name] = registeredResourceTemplate;
+        readCallback as ReadResourceTemplateCallback
+      );
+
+      this.setResourceRequestHandlers();
+      this.sendResourceListChanged();
+      return registeredResourceTemplate;
+    }
+  }
+
+  /**
+   * Registers a resource with a config object and callback.
+   * For static resources, use a URI string. For dynamic resources, use a ResourceTemplate.
+   */
+  registerResource(
+    name: string,
+    uriOrTemplate: string,
+    config: ResourceMetadata,
+    readCallback: ReadResourceCallback
+  ): RegisteredResource;
+  registerResource(
+    name: string,
+    uriOrTemplate: ResourceTemplate,
+    config: ResourceMetadata,
+    readCallback: ReadResourceTemplateCallback
+  ): RegisteredResourceTemplate;
+  registerResource(
+    name: string,
+    uriOrTemplate: string | ResourceTemplate,
+    config: ResourceMetadata,
+    readCallback: ReadResourceCallback | ReadResourceTemplateCallback
+  ): RegisteredResource | RegisteredResourceTemplate {
+    if (typeof uriOrTemplate === "string") {
+      if (this._registeredResources[uriOrTemplate]) {
+        throw new Error(`Resource ${uriOrTemplate} is already registered`);
+      }
+
+      const registeredResource = this._createRegisteredResource(
+        name,
+        (config as BaseMetadata).title,
+        uriOrTemplate,
+        config,
+        readCallback as ReadResourceCallback
+      );
+
+      this.setResourceRequestHandlers();
+      this.sendResourceListChanged();
+      return registeredResource;
+    } else {
+      if (this._registeredResourceTemplates[name]) {
+        throw new Error(`Resource template ${name} is already registered`);
+      }
+
+      const registeredResourceTemplate = this._createRegisteredResourceTemplate(
+        name,
+        (config as BaseMetadata).title,
+        uriOrTemplate,
+        config,
+        readCallback as ReadResourceTemplateCallback
+      );
 
       this.setResourceRequestHandlers();
       this.sendResourceListChanged();
@@ -633,8 +666,108 @@ export class McpServer {
     }
   }
 
+  private _createRegisteredResource(
+    name: string,
+    title: string | undefined,
+    uri: string,
+    metadata: ResourceMetadata | undefined,
+    readCallback: ReadResourceCallback
+  ): RegisteredResource {
+    const registeredResource: RegisteredResource = {
+      name,
+      title,
+      metadata,
+      readCallback,
+      enabled: true,
+      disable: () => registeredResource.update({ enabled: false }),
+      enable: () => registeredResource.update({ enabled: true }),
+      remove: () => registeredResource.update({ uri: null }),
+      update: (updates) => {
+        if (typeof updates.uri !== "undefined" && updates.uri !== uri) {
+          delete this._registeredResources[uri]
+          if (updates.uri) this._registeredResources[updates.uri] = registeredResource
+        }
+        if (typeof updates.name !== "undefined") registeredResource.name = updates.name
+        if (typeof updates.title !== "undefined") registeredResource.title = updates.title
+        if (typeof updates.metadata !== "undefined") registeredResource.metadata = updates.metadata
+        if (typeof updates.callback !== "undefined") registeredResource.readCallback = updates.callback
+        if (typeof updates.enabled !== "undefined") registeredResource.enabled = updates.enabled
+        this.sendResourceListChanged()
+      },
+    };
+    this._registeredResources[uri] = registeredResource;
+    return registeredResource;
+  }
+
+  private _createRegisteredResourceTemplate(
+    name: string,
+    title: string | undefined,
+    template: ResourceTemplate,
+    metadata: ResourceMetadata | undefined,
+    readCallback: ReadResourceTemplateCallback
+  ): RegisteredResourceTemplate {
+    const registeredResourceTemplate: RegisteredResourceTemplate = {
+      resourceTemplate: template,
+      title,
+      metadata,
+      readCallback,
+      enabled: true,
+      disable: () => registeredResourceTemplate.update({ enabled: false }),
+      enable: () => registeredResourceTemplate.update({ enabled: true }),
+      remove: () => registeredResourceTemplate.update({ name: null }),
+      update: (updates) => {
+        if (typeof updates.name !== "undefined" && updates.name !== name) {
+          delete this._registeredResourceTemplates[name]
+          if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate
+        }
+        if (typeof updates.title !== "undefined") registeredResourceTemplate.title = updates.title
+        if (typeof updates.template !== "undefined") registeredResourceTemplate.resourceTemplate = updates.template
+        if (typeof updates.metadata !== "undefined") registeredResourceTemplate.metadata = updates.metadata
+        if (typeof updates.callback !== "undefined") registeredResourceTemplate.readCallback = updates.callback
+        if (typeof updates.enabled !== "undefined") registeredResourceTemplate.enabled = updates.enabled
+        this.sendResourceListChanged()
+      },
+    };
+    this._registeredResourceTemplates[name] = registeredResourceTemplate;
+    return registeredResourceTemplate;
+  }
+
+  private _createRegisteredPrompt(
+    name: string,
+    title: string | undefined,
+    description: string | undefined,
+    argsSchema: PromptArgsRawShape | undefined,
+    callback: PromptCallback<PromptArgsRawShape | undefined>
+  ): RegisteredPrompt {
+    const registeredPrompt: RegisteredPrompt = {
+      title,
+      description,
+      argsSchema: argsSchema === undefined ? undefined : z.object(argsSchema),
+      callback,
+      enabled: true,
+      disable: () => registeredPrompt.update({ enabled: false }),
+      enable: () => registeredPrompt.update({ enabled: true }),
+      remove: () => registeredPrompt.update({ name: null }),
+      update: (updates) => {
+        if (typeof updates.name !== "undefined" && updates.name !== name) {
+          delete this._registeredPrompts[name]
+          if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt
+        }
+        if (typeof updates.title !== "undefined") registeredPrompt.title = updates.title
+        if (typeof updates.description !== "undefined") registeredPrompt.description = updates.description
+        if (typeof updates.argsSchema !== "undefined") registeredPrompt.argsSchema = z.object(updates.argsSchema)
+        if (typeof updates.callback !== "undefined") registeredPrompt.callback = updates.callback
+        if (typeof updates.enabled !== "undefined") registeredPrompt.enabled = updates.enabled
+        this.sendPromptListChanged()
+      },
+    };
+    this._registeredPrompts[name] = registeredPrompt;
+    return registeredPrompt;
+  }
+
   private _createRegisteredTool(
     name: string,
+    title: string | undefined,
     description: string | undefined,
     inputSchema: ZodRawShape | undefined,
     outputSchema: ZodRawShape | undefined,
@@ -642,6 +775,7 @@ export class McpServer {
     callback: ToolCallback<ZodRawShape | undefined>
   ): RegisteredTool {
     const registeredTool: RegisteredTool = {
+      title,
       description,
       inputSchema:
         inputSchema === undefined ? undefined : z.object(inputSchema),
@@ -658,6 +792,7 @@ export class McpServer {
           delete this._registeredTools[name]
           if (updates.name) this._registeredTools[updates.name] = registeredTool
         }
+        if (typeof updates.title !== "undefined") registeredTool.title = updates.title
         if (typeof updates.description !== "undefined") registeredTool.description = updates.description
         if (typeof updates.paramsSchema !== "undefined") registeredTool.inputSchema = z.object(updates.paramsSchema)
         if (typeof updates.callback !== "undefined") registeredTool.callback = updates.callback
@@ -779,7 +914,7 @@ export class McpServer {
     }
     const callback = rest[0] as ToolCallback<ZodRawShape | undefined>;
 
-    return this._createRegisteredTool(name, description, inputSchema, outputSchema, annotations, callback)
+    return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, callback)
   }
 
   /**
@@ -788,6 +923,7 @@ export class McpServer {
   registerTool<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape>(
     name: string,
     config: {
+      title?: string;
       description?: string;
       inputSchema?: InputArgs;
       outputSchema?: OutputArgs;
@@ -799,16 +935,17 @@ export class McpServer {
       throw new Error(`Tool ${name} is already registered`);
     }
 
-    const { description, inputSchema, outputSchema, annotations } = config;
+    const { title, description, inputSchema, outputSchema, annotations } = config;
 
     return this._createRegisteredTool(
       name,
+      title,
       description,
       inputSchema,
       outputSchema,
       annotations,
       cb as ToolCallback<ZodRawShape | undefined>
-    )
+    );
   }
 
   /**
@@ -856,27 +993,13 @@ export class McpServer {
     }
 
     const cb = rest[0] as PromptCallback<PromptArgsRawShape | undefined>;
-    const registeredPrompt: RegisteredPrompt = {
+    const registeredPrompt = this._createRegisteredPrompt(
+      name,
+      undefined,
       description,
-      argsSchema: argsSchema === undefined ? undefined : z.object(argsSchema),
-      callback: cb,
-      enabled: true,
-      disable: () => registeredPrompt.update({ enabled: false }),
-      enable: () => registeredPrompt.update({ enabled: true }),
-      remove: () => registeredPrompt.update({ name: null }),
-      update: (updates) => {
-        if (typeof updates.name !== "undefined" && updates.name !== name) {
-          delete this._registeredPrompts[name]
-          if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt
-        }
-        if (typeof updates.description !== "undefined") registeredPrompt.description = updates.description
-        if (typeof updates.argsSchema !== "undefined") registeredPrompt.argsSchema = z.object(updates.argsSchema)
-        if (typeof updates.callback !== "undefined") registeredPrompt.callback = updates.callback
-        if (typeof updates.enabled !== "undefined") registeredPrompt.enabled = updates.enabled
-        this.sendPromptListChanged()
-      },
-    };
-    this._registeredPrompts[name] = registeredPrompt;
+      argsSchema,
+      cb
+    );
 
     this.setPromptRequestHandlers();
     this.sendPromptListChanged()
@@ -884,6 +1007,38 @@ export class McpServer {
     return registeredPrompt
   }
 
+  /**
+   * Registers a prompt with a config object and callback.
+   */
+  registerPrompt<Args extends PromptArgsRawShape>(
+    name: string,
+    config: {
+      title?: string;
+      description?: string;
+      argsSchema?: Args;
+    },
+    cb: PromptCallback<Args>
+  ): RegisteredPrompt {
+    if (this._registeredPrompts[name]) {
+      throw new Error(`Prompt ${name} is already registered`);
+    }
+
+    const { title, description, argsSchema } = config;
+
+    const registeredPrompt = this._createRegisteredPrompt(
+      name,
+      title,
+      description,
+      argsSchema,
+      cb as PromptCallback<PromptArgsRawShape | undefined>
+    );
+
+    this.setPromptRequestHandlers();
+    this.sendPromptListChanged()
+
+    return registeredPrompt;
+  }
+
   /**
    * Checks if the server is connected to a transport.
    * @returns True if the server is connected
@@ -925,6 +1080,9 @@ export class McpServer {
  */
 export type CompleteResourceTemplateCallback = (
   value: string,
+  context?: {
+    arguments?: Record<string, string>;
+  },
 ) => string[] | Promise<string[]>;
 
 /**
@@ -999,6 +1157,7 @@ export type ToolCallback<Args extends undefined | ZodRawShape = undefined> =
   : (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;
 
 export type RegisteredTool = {
+  title?: string;
   description?: string;
   inputSchema?: AnyZodObject;
   outputSchema?: AnyZodObject;
@@ -1008,15 +1167,16 @@ export type RegisteredTool = {
   enable(): void;
   disable(): void;
   update<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape>(
-    updates: { 
-      name?: string | null, 
-      description?: string, 
-      paramsSchema?: InputArgs, 
-      outputSchema?: OutputArgs, 
-      annotations?: ToolAnnotations, 
-      callback?: ToolCallback<InputArgs>, 
-      enabled?: boolean 
-  }): void
+    updates: {
+      name?: string | null,
+      title?: string,
+      description?: string,
+      paramsSchema?: InputArgs,
+      outputSchema?: OutputArgs,
+      annotations?: ToolAnnotations,
+      callback?: ToolCallback<InputArgs>,
+      enabled?: boolean
+    }): void
   remove(): void
 };
 
@@ -1064,12 +1224,13 @@ export type ReadResourceCallback = (
 
 export type RegisteredResource = {
   name: string;
+  title?: string;
   metadata?: ResourceMetadata;
   readCallback: ReadResourceCallback;
   enabled: boolean;
   enable(): void;
   disable(): void;
-  update(updates: { name?: string, uri?: string | null, metadata?: ResourceMetadata, callback?: ReadResourceCallback, enabled?: boolean }): void
+  update(updates: { name?: string, title?: string, uri?: string | null, metadata?: ResourceMetadata, callback?: ReadResourceCallback, enabled?: boolean }): void
   remove(): void
 };
 
@@ -1084,12 +1245,13 @@ export type ReadResourceTemplateCallback = (
 
 export type RegisteredResourceTemplate = {
   resourceTemplate: ResourceTemplate;
+  title?: string;
   metadata?: ResourceMetadata;
   readCallback: ReadResourceTemplateCallback;
   enabled: boolean;
   enable(): void;
   disable(): void;
-  update(updates: { name?: string | null, template?: ResourceTemplate, metadata?: ResourceMetadata, callback?: ReadResourceTemplateCallback, enabled?: boolean }): void
+  update(updates: { name?: string | null, title?: string, template?: ResourceTemplate, metadata?: ResourceMetadata, callback?: ReadResourceTemplateCallback, enabled?: boolean }): void
   remove(): void
 };
 
@@ -1109,13 +1271,14 @@ export type PromptCallback<
   : (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => GetPromptResult | Promise<GetPromptResult>;
 
 export type RegisteredPrompt = {
+  title?: string;
   description?: string;
   argsSchema?: ZodObject<PromptArgsRawShape>;
   callback: PromptCallback<undefined | PromptArgsRawShape>;
   enabled: boolean;
   enable(): void;
   disable(): void;
-  update<Args extends PromptArgsRawShape>(updates: { name?: string | null, description?: string, argsSchema?: Args, callback?: PromptCallback<Args>, enabled?: boolean }): void
+  update<Args extends PromptArgsRawShape>(updates: { name?: string | null, title?: string, description?: string, argsSchema?: Args, callback?: PromptCallback<Args>, enabled?: boolean }): void
   remove(): void
 };
 
diff --git a/src/server/sse.test.ts b/src/server/sse.test.ts
index 2fd2c04..a7f1809 100644
--- a/src/server/sse.test.ts
+++ b/src/server/sse.test.ts
@@ -1,20 +1,176 @@
 import http from 'http'; 
 import { jest } from '@jest/globals';
 import { SSEServerTransport } from './sse.js'; 
+import { McpServer } from './mcp.js';
+import { createServer, type Server } from "node:http";
+import { AddressInfo } from "node:net";
+import { z } from 'zod';
+import { CallToolResult, JSONRPCMessage } from 'src/types.js';
 
 const createMockResponse = () => {
   const res = {
-    writeHead: jest.fn<http.ServerResponse['writeHead']>(),
-    write: jest.fn<http.ServerResponse['write']>().mockReturnValue(true),
-    on: jest.fn<http.ServerResponse['on']>(),
+    writeHead: jest.fn<http.ServerResponse['writeHead']>().mockReturnThis(),
+    write: jest.fn<http.ServerResponse['write']>().mockReturnThis(),
+    on: jest.fn<http.ServerResponse['on']>().mockReturnThis(),
+    end: jest.fn<http.ServerResponse['end']>().mockReturnThis(),
   };
-  res.writeHead.mockReturnThis();
-  res.on.mockReturnThis();
   
-  return res as unknown as http.ServerResponse;
+  return res as unknown as jest.Mocked<http.ServerResponse>;
 };
 
+const createMockRequest = ({ headers = {}, body }: { headers?: Record<string, string>, body?: string } = {}) => {
+  const mockReq = {
+    headers,
+    body: body ? body : undefined,
+    auth: {
+      token: 'test-token',
+    },
+    on: jest.fn<http.IncomingMessage['on']>().mockImplementation((event, listener) => {
+      const mockListener = listener as unknown as (...args: unknown[]) => void;
+      if (event === 'data') {
+        mockListener(Buffer.from(body || '') as unknown as Error);
+      }
+      if (event === 'error') {
+        mockListener(new Error('test'));
+      }
+      if (event === 'end') {
+        mockListener();
+      }
+      if (event === 'close') {
+        setTimeout(listener, 100);
+      }
+      return mockReq;
+    }),
+    listeners: jest.fn<http.IncomingMessage['listeners']>(),
+    removeListener: jest.fn<http.IncomingMessage['removeListener']>(),
+  } as unknown as http.IncomingMessage;
+
+  return mockReq;
+};
+
+/**
+ * Helper to create and start test HTTP server with MCP setup
+ */
+async function createTestServerWithSse(args: {
+  mockRes: http.ServerResponse;
+}): Promise<{
+  server: Server;
+  transport: SSEServerTransport;
+  mcpServer: McpServer;
+  baseUrl: URL;
+  sessionId: string
+  serverPort: number;
+}> {
+  const mcpServer = new McpServer(
+    { name: "test-server", version: "1.0.0" },
+    { capabilities: { logging: {} } }
+  );
+
+  mcpServer.tool(
+    "greet",
+    "A simple greeting tool",
+    { name: z.string().describe("Name to greet") },
+    async ({ name }): Promise<CallToolResult> => {
+      return { content: [{ type: "text", text: `Hello, ${name}!` }] };
+    }
+  );
+
+  const endpoint = '/messages';
+
+  const transport = new SSEServerTransport(endpoint, args.mockRes);
+  const sessionId = transport.sessionId;
+
+  await mcpServer.connect(transport);
+
+  const server = createServer(async (req, res) => {
+    try {
+        await transport.handlePostMessage(req, res);
+    } catch (error) {
+      console.error("Error handling request:", error);
+      if (!res.headersSent) res.writeHead(500).end();
+    }
+  });
+
+  const baseUrl = await new Promise<URL>((resolve) => {
+    server.listen(0, "127.0.0.1", () => {
+      const addr = server.address() as AddressInfo;
+      resolve(new URL(`http://127.0.0.1:${addr.port}`));
+    });
+  });
+
+  const port = (server.address() as AddressInfo).port;
+
+  return { server, transport, mcpServer, baseUrl, sessionId, serverPort: port };
+}
+
+async function readAllSSEEvents(response: Response): Promise<string[]> {
+  const reader = response.body?.getReader();
+  if (!reader) throw new Error('No readable stream');
+  
+  const events: string[] = [];
+  const decoder = new TextDecoder();
+  
+  try {
+    while (true) {
+      const { done, value } = await reader.read();
+      if (done) break;
+      
+      if (value) {
+        events.push(decoder.decode(value));
+      }
+    }
+  } finally {
+    reader.releaseLock();
+  }
+  
+  return events;
+}
+
+/**
+ * Helper to send JSON-RPC request
+ */
+async function sendSsePostRequest(baseUrl: URL, message: JSONRPCMessage | JSONRPCMessage[], sessionId?: string, extraHeaders?: Record<string, string>): Promise<Response> {
+  const headers: Record<string, string> = {
+    "Content-Type": "application/json",
+    Accept: "application/json, text/event-stream",
+    ...extraHeaders
+  };
+
+  if (sessionId) {
+    baseUrl.searchParams.set('sessionId', sessionId);
+  }
+
+  return fetch(baseUrl, {
+    method: "POST",
+    headers,
+    body: JSON.stringify(message),
+  });
+}
+
 describe('SSEServerTransport', () => {
+
+  async function initializeServer(baseUrl: URL): Promise<void> {
+    const response = await sendSsePostRequest(baseUrl, {
+      jsonrpc: "2.0",
+      method: "initialize",
+      params: {
+        clientInfo: { name: "test-client", version: "1.0" },
+        protocolVersion: "2025-03-26",
+        capabilities: {
+        },
+      },
+  
+      id: "init-1",
+    } as JSONRPCMessage);
+
+    expect(response.status).toBe(202);
+
+    const text = await readAllSSEEvents(response);
+
+    expect(text).toHaveLength(1);
+    expect(text[0]).toBe('Accepted');
+  }
+
   describe('start method', () => { 
     it('should correctly append sessionId to a simple relative endpoint', async () => { 
       const mockRes = createMockResponse();
@@ -105,5 +261,456 @@ describe('SSEServerTransport', () => {
         `event: endpoint\ndata: /?sessionId=${expectedSessionId}\n\n`
       );
     });
+
+  /**
+   * Test: Tool With Request Info
+   */
+  it("should pass request info to tool callback", async () => {
+    const mockRes = createMockResponse();
+    const { mcpServer, baseUrl, sessionId, serverPort } = await createTestServerWithSse({ mockRes });
+    await initializeServer(baseUrl);
+
+    mcpServer.tool(
+      "test-request-info",
+      "A simple test tool with request info",
+      { name: z.string().describe("Name to greet") },
+      async ({ name }, { requestInfo }): Promise<CallToolResult> => {
+        return { content: [{ type: "text", text: `Hello, ${name}!` }, { type: "text", text: `${JSON.stringify(requestInfo)}` }] };
+      }
+    );
+   
+    const toolCallMessage: JSONRPCMessage = {
+      jsonrpc: "2.0",
+      method: "tools/call",
+      params: {
+        name: "test-request-info",
+        arguments: {
+          name: "Test User",
+        },
+      },
+      id: "call-1",
+    };
+
+    const response = await sendSsePostRequest(baseUrl, toolCallMessage, sessionId);
+
+    expect(response.status).toBe(202);
+
+    expect(mockRes.write).toHaveBeenCalledWith(`event: endpoint\ndata: /messages?sessionId=${sessionId}\n\n`);
+
+    const expectedMessage = {
+      result: {
+        content: [
+          {
+            type: "text",
+            text: "Hello, Test User!",
+          },
+          {
+            type: "text",
+            text: JSON.stringify({
+              headers: {
+                host: `127.0.0.1:${serverPort}`,
+                connection: 'keep-alive',
+                'content-type': 'application/json',
+                accept: 'application/json, text/event-stream',
+                'accept-language': '*',
+                'sec-fetch-mode': 'cors',
+                'user-agent': 'node',
+                'accept-encoding': 'gzip, deflate',
+                'content-length': '124'
+              },
+            })
+          },
+        ],
+      },
+      jsonrpc: "2.0",
+      id: "call-1",
+    };
+    expect(mockRes.write).toHaveBeenCalledWith(`event: message\ndata: ${JSON.stringify(expectedMessage)}\n\n`);
+  });
+  });
+
+  describe('handlePostMessage method', () => {
+    it('should return 500 if server has not started', async () => {
+      const mockReq = createMockRequest();
+      const mockRes = createMockResponse();
+      const endpoint = '/messages';
+      const transport = new SSEServerTransport(endpoint, mockRes);
+
+      const error = 'SSE connection not established';
+      await expect(transport.handlePostMessage(mockReq, mockRes))
+        .rejects.toThrow(error);
+      expect(mockRes.writeHead).toHaveBeenCalledWith(500);
+      expect(mockRes.end).toHaveBeenCalledWith(error);
+    });
+
+    it('should return 400 if content-type is not application/json', async () => {
+      const mockReq = createMockRequest({ headers: { 'content-type': 'text/plain' } });
+      const mockRes = createMockResponse();
+      const endpoint = '/messages';
+      const transport = new SSEServerTransport(endpoint, mockRes);
+      await transport.start();
+
+      transport.onerror = jest.fn();
+      const error = 'Unsupported content-type: text/plain';
+      await expect(transport.handlePostMessage(mockReq, mockRes))
+        .resolves.toBe(undefined);
+      expect(mockRes.writeHead).toHaveBeenCalledWith(400);
+      expect(mockRes.end).toHaveBeenCalledWith(expect.stringContaining(error));
+      expect(transport.onerror).toHaveBeenCalledWith(new Error(error));
+    });
+
+    it('should return 400 if message has not a valid schema', async () => {
+      const invalidMessage = JSON.stringify({
+        // missing jsonrpc field
+        method: 'call',
+        params: [1, 2, 3],
+        id: 1,
+      })
+      const mockReq = createMockRequest({
+        headers: { 'content-type': 'application/json' },
+        body: invalidMessage,
+      });
+      const mockRes = createMockResponse();
+      const endpoint = '/messages';
+      const transport = new SSEServerTransport(endpoint, mockRes);
+      await transport.start();
+
+      transport.onmessage = jest.fn();
+      await transport.handlePostMessage(mockReq, mockRes);
+      expect(mockRes.writeHead).toHaveBeenCalledWith(400);
+      expect(transport.onmessage).not.toHaveBeenCalled();
+      expect(mockRes.end).toHaveBeenCalledWith(`Invalid message: ${invalidMessage}`);
+    });
+
+    it('should return 202 if message has a valid schema', async () => {
+      const validMessage = JSON.stringify({
+        jsonrpc: "2.0",
+        method: 'call',
+        params: {
+          a: 1,
+          b: 2,
+          c: 3,
+        },
+        id: 1
+      })
+      const mockReq = createMockRequest({
+        headers: { 'content-type': 'application/json' },
+        body: validMessage,
+      });
+      const mockRes = createMockResponse();
+      const endpoint = '/messages';
+      const transport = new SSEServerTransport(endpoint, mockRes);
+      await transport.start();
+
+      transport.onmessage = jest.fn();
+      await transport.handlePostMessage(mockReq, mockRes);
+      expect(mockRes.writeHead).toHaveBeenCalledWith(202);
+      expect(mockRes.end).toHaveBeenCalledWith('Accepted');
+      expect(transport.onmessage).toHaveBeenCalledWith({
+        jsonrpc: "2.0",
+        method: 'call',
+        params: {
+          a: 1,
+          b: 2,
+          c: 3,
+        },
+        id: 1
+      }, {
+        authInfo: {
+          token: 'test-token',
+        },
+        requestInfo: {
+          headers: {
+            'content-type': 'application/json',
+          },
+        },
+      });
+    });
+  });
+
+  describe('close method', () => {
+    it('should call onclose', async () => {
+      const mockRes = createMockResponse();
+      const endpoint = '/messages';
+      const transport = new SSEServerTransport(endpoint, mockRes);
+      await transport.start();
+      transport.onclose = jest.fn();
+      await transport.close();
+      expect(transport.onclose).toHaveBeenCalled();
+    });
+  });
+
+  describe('send method', () => {
+    it('should call onsend', async () => {
+      const mockRes = createMockResponse();
+      const endpoint = '/messages';
+      const transport = new SSEServerTransport(endpoint, mockRes);
+      await transport.start();
+      expect(mockRes.write).toHaveBeenCalledTimes(1);
+      expect(mockRes.write).toHaveBeenCalledWith(
+        expect.stringContaining('event: endpoint'));
+      expect(mockRes.write).toHaveBeenCalledWith(
+        expect.stringContaining(`data: /messages?sessionId=${transport.sessionId}`));
+    });
+  });
+
+  describe('DNS rebinding protection', () => {
+    beforeEach(() => {
+      jest.clearAllMocks();
+    });
+
+    describe('Host header validation', () => {
+      it('should accept requests with allowed host headers', async () => {
+        const mockRes = createMockResponse();
+        const transport = new SSEServerTransport('/messages', mockRes, {
+          allowedHosts: ['localhost:3000', 'example.com'],
+          enableDnsRebindingProtection: true,
+        });
+        await transport.start();
+
+        const mockReq = createMockRequest({
+          headers: {
+            host: 'localhost:3000',
+            'content-type': 'application/json',
+          }
+        });
+        const mockHandleRes = createMockResponse();
+
+        await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' });
+
+        expect(mockHandleRes.writeHead).toHaveBeenCalledWith(202);
+        expect(mockHandleRes.end).toHaveBeenCalledWith('Accepted');
+      });
+
+      it('should reject requests with disallowed host headers', async () => {
+        const mockRes = createMockResponse();
+        const transport = new SSEServerTransport('/messages', mockRes, {
+          allowedHosts: ['localhost:3000'],
+          enableDnsRebindingProtection: true,
+        });
+        await transport.start();
+
+        const mockReq = createMockRequest({
+          headers: {
+            host: 'evil.com',
+            'content-type': 'application/json',
+          }
+        });
+        const mockHandleRes = createMockResponse();
+
+        await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' });
+
+        expect(mockHandleRes.writeHead).toHaveBeenCalledWith(403);
+        expect(mockHandleRes.end).toHaveBeenCalledWith('Invalid Host header: evil.com');
+      });
+
+      it('should reject requests without host header when allowedHosts is configured', async () => {
+        const mockRes = createMockResponse();
+        const transport = new SSEServerTransport('/messages', mockRes, {
+          allowedHosts: ['localhost:3000'],
+          enableDnsRebindingProtection: true,
+        });
+        await transport.start();
+
+        const mockReq = createMockRequest({
+          headers: {
+            'content-type': 'application/json',
+          }
+        });
+        const mockHandleRes = createMockResponse();
+
+        await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' });
+
+        expect(mockHandleRes.writeHead).toHaveBeenCalledWith(403);
+        expect(mockHandleRes.end).toHaveBeenCalledWith('Invalid Host header: undefined');
+      });
+    });
+
+    describe('Origin header validation', () => {
+      it('should accept requests with allowed origin headers', async () => {
+        const mockRes = createMockResponse();
+        const transport = new SSEServerTransport('/messages', mockRes, {
+          allowedOrigins: ['http://localhost:3000', 'https://example.com'],
+          enableDnsRebindingProtection: true,
+        });
+        await transport.start();
+
+        const mockReq = createMockRequest({
+          headers: {
+            origin: 'http://localhost:3000',
+            'content-type': 'application/json',
+          }
+        });
+        const mockHandleRes = createMockResponse();
+
+        await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' });
+
+        expect(mockHandleRes.writeHead).toHaveBeenCalledWith(202);
+        expect(mockHandleRes.end).toHaveBeenCalledWith('Accepted');
+      });
+
+      it('should reject requests with disallowed origin headers', async () => {
+        const mockRes = createMockResponse();
+        const transport = new SSEServerTransport('/messages', mockRes, {
+          allowedOrigins: ['http://localhost:3000'],
+          enableDnsRebindingProtection: true,
+        });
+        await transport.start();
+
+        const mockReq = createMockRequest({
+          headers: {
+            origin: 'http://evil.com',
+            'content-type': 'application/json',
+          }
+        });
+        const mockHandleRes = createMockResponse();
+
+        await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' });
+
+        expect(mockHandleRes.writeHead).toHaveBeenCalledWith(403);
+        expect(mockHandleRes.end).toHaveBeenCalledWith('Invalid Origin header: http://evil.com');
+      });
+    });
+
+    describe('Content-Type validation', () => {
+      it('should accept requests with application/json content-type', async () => {
+        const mockRes = createMockResponse();
+        const transport = new SSEServerTransport('/messages', mockRes);
+        await transport.start();
+
+        const mockReq = createMockRequest({
+          headers: {
+            'content-type': 'application/json',
+          }
+        });
+        const mockHandleRes = createMockResponse();
+
+        await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' });
+
+        expect(mockHandleRes.writeHead).toHaveBeenCalledWith(202);
+        expect(mockHandleRes.end).toHaveBeenCalledWith('Accepted');
+      });
+
+      it('should accept requests with application/json with charset', async () => {
+        const mockRes = createMockResponse();
+        const transport = new SSEServerTransport('/messages', mockRes);
+        await transport.start();
+
+        const mockReq = createMockRequest({
+          headers: {
+            'content-type': 'application/json; charset=utf-8',
+          }
+        });
+        const mockHandleRes = createMockResponse();
+
+        await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' });
+
+        expect(mockHandleRes.writeHead).toHaveBeenCalledWith(202);
+        expect(mockHandleRes.end).toHaveBeenCalledWith('Accepted');
+      });
+
+      it('should reject requests with non-application/json content-type when protection is enabled', async () => {
+        const mockRes = createMockResponse();
+        const transport = new SSEServerTransport('/messages', mockRes);
+        await transport.start();
+
+        const mockReq = createMockRequest({
+          headers: {
+            'content-type': 'text/plain',
+          }
+        });
+        const mockHandleRes = createMockResponse();
+
+        await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' });
+
+        expect(mockHandleRes.writeHead).toHaveBeenCalledWith(400);
+        expect(mockHandleRes.end).toHaveBeenCalledWith('Error: Unsupported content-type: text/plain');
+      });
+    });
+
+    describe('enableDnsRebindingProtection option', () => {
+      it('should skip all validations when enableDnsRebindingProtection is false', async () => {
+        const mockRes = createMockResponse();
+        const transport = new SSEServerTransport('/messages', mockRes, {
+          allowedHosts: ['localhost:3000'],
+          allowedOrigins: ['http://localhost:3000'],
+          enableDnsRebindingProtection: false,
+        });
+        await transport.start();
+
+        const mockReq = createMockRequest({
+          headers: {
+            host: 'evil.com',
+            origin: 'http://evil.com',
+            'content-type': 'text/plain',
+          }
+        });
+        const mockHandleRes = createMockResponse();
+
+        await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' });
+
+        // Should pass even with invalid headers because protection is disabled
+        expect(mockHandleRes.writeHead).toHaveBeenCalledWith(400);
+        // The error should be from content-type parsing, not DNS rebinding protection
+        expect(mockHandleRes.end).toHaveBeenCalledWith('Error: Unsupported content-type: text/plain');
+      });
+    });
+
+    describe('Combined validations', () => {
+      it('should validate both host and origin when both are configured', async () => {
+        const mockRes = createMockResponse();
+        const transport = new SSEServerTransport('/messages', mockRes, {
+          allowedHosts: ['localhost:3000'],
+          allowedOrigins: ['http://localhost:3000'],
+          enableDnsRebindingProtection: true,
+        });
+        await transport.start();
+
+        // Valid host, invalid origin
+        const mockReq1 = createMockRequest({
+          headers: {
+            host: 'localhost:3000',
+            origin: 'http://evil.com',
+            'content-type': 'application/json',
+          }
+        });
+        const mockHandleRes1 = createMockResponse();
+
+        await transport.handlePostMessage(mockReq1, mockHandleRes1, { jsonrpc: '2.0', method: 'test' });
+
+        expect(mockHandleRes1.writeHead).toHaveBeenCalledWith(403);
+        expect(mockHandleRes1.end).toHaveBeenCalledWith('Invalid Origin header: http://evil.com');
+
+        // Invalid host, valid origin
+        const mockReq2 = createMockRequest({
+          headers: {
+            host: 'evil.com',
+            origin: 'http://localhost:3000',
+            'content-type': 'application/json',
+          }
+        });
+        const mockHandleRes2 = createMockResponse();
+
+        await transport.handlePostMessage(mockReq2, mockHandleRes2, { jsonrpc: '2.0', method: 'test' });
+
+        expect(mockHandleRes2.writeHead).toHaveBeenCalledWith(403);
+        expect(mockHandleRes2.end).toHaveBeenCalledWith('Invalid Host header: evil.com');
+
+        // Both valid
+        const mockReq3 = createMockRequest({
+          headers: {
+            host: 'localhost:3000',
+            origin: 'http://localhost:3000',
+            'content-type': 'application/json',
+          }
+        });
+        const mockHandleRes3 = createMockResponse();
+
+        await transport.handlePostMessage(mockReq3, mockHandleRes3, { jsonrpc: '2.0', method: 'test' });
+
+        expect(mockHandleRes3.writeHead).toHaveBeenCalledWith(202);
+        expect(mockHandleRes3.end).toHaveBeenCalledWith('Accepted');
+      });
+    });
   });
 });
diff --git a/src/server/sse.ts b/src/server/sse.ts
index 03f6fef..e072568 100644
--- a/src/server/sse.ts
+++ b/src/server/sse.ts
@@ -1,7 +1,7 @@
 import { randomUUID } from "node:crypto";
 import { IncomingMessage, ServerResponse } from "node:http";
 import { Transport } from "../shared/transport.js";
-import { JSONRPCMessage, JSONRPCMessageSchema } from "../types.js";
+import { JSONRPCMessage, JSONRPCMessageSchema, MessageExtraInfo, RequestInfo } from "../types.js";
 import getRawBody from "raw-body";
 import contentType from "content-type";
 import { AuthInfo } from "./auth/types.js";
@@ -9,6 +9,29 @@ import { URL } from 'url';
 
 const MAXIMUM_MESSAGE_SIZE = "4mb";
 
+/**
+ * Configuration options for SSEServerTransport.
+ */
+export interface SSEServerTransportOptions {
+  /**
+   * List of allowed host header values for DNS rebinding protection.
+   * If not specified, host validation is disabled.
+   */
+  allowedHosts?: string[];
+  
+  /**
+   * List of allowed origin header values for DNS rebinding protection.
+   * If not specified, origin validation is disabled.
+   */
+  allowedOrigins?: string[];
+  
+  /**
+   * Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured).
+   * Default is false for backwards compatibility.
+   */
+  enableDnsRebindingProtection?: boolean;
+}
+
 /**
  * Server transport for SSE: this will send messages over an SSE connection and receive messages from HTTP POST requests.
  *
@@ -17,10 +40,10 @@ const MAXIMUM_MESSAGE_SIZE = "4mb";
 export class SSEServerTransport implements Transport {
   private _sseResponse?: ServerResponse;
   private _sessionId: string;
-
+  private _options: SSEServerTransportOptions;
   onclose?: () => void;
   onerror?: (error: Error) => void;
-  onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void;
+  onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void;
 
   /**
    * Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`.
@@ -28,8 +51,39 @@ export class SSEServerTransport implements Transport {
   constructor(
     private _endpoint: string,
     private res: ServerResponse,
+    options?: SSEServerTransportOptions,
   ) {
     this._sessionId = randomUUID();
+    this._options = options || {enableDnsRebindingProtection: false};
+  }
+
+  /**
+   * Validates request headers for DNS rebinding protection.
+   * @returns Error message if validation fails, undefined if validation passes.
+   */
+  private validateRequestHeaders(req: IncomingMessage): string | undefined {
+    // Skip validation if protection is not enabled
+    if (!this._options.enableDnsRebindingProtection) {
+      return undefined;
+    }
+
+    // Validate Host header if allowedHosts is configured
+    if (this._options.allowedHosts && this._options.allowedHosts.length > 0) {
+      const hostHeader = req.headers.host;
+      if (!hostHeader || !this._options.allowedHosts.includes(hostHeader)) {
+        return `Invalid Host header: ${hostHeader}`;
+      }
+    }
+
+    // Validate Origin header if allowedOrigins is configured
+    if (this._options.allowedOrigins && this._options.allowedOrigins.length > 0) {
+      const originHeader = req.headers.origin;
+      if (!originHeader || !this._options.allowedOrigins.includes(originHeader)) {
+        return `Invalid Origin header: ${originHeader}`;
+      }
+    }
+
+    return undefined;
   }
 
   /**
@@ -86,13 +140,23 @@ export class SSEServerTransport implements Transport {
       res.writeHead(500).end(message);
       throw new Error(message);
     }
+
+    // Validate request headers for DNS rebinding protection
+    const validationError = this.validateRequestHeaders(req);
+    if (validationError) {
+      res.writeHead(403).end(validationError);
+      this.onerror?.(new Error(validationError));
+      return;
+    }
+
     const authInfo: AuthInfo | undefined = req.auth;
+    const requestInfo: RequestInfo = { headers: req.headers };
 
     let body: string | unknown;
     try {
       const ct = contentType.parse(req.headers["content-type"] ?? "");
       if (ct.type !== "application/json") {
-        throw new Error(`Unsupported content-type: ${ct}`);
+        throw new Error(`Unsupported content-type: ${ct.type}`);
       }
 
       body = parsedBody ?? await getRawBody(req, {
@@ -106,7 +170,7 @@ export class SSEServerTransport implements Transport {
     }
 
     try {
-      await this.handleMessage(typeof body === 'string' ? JSON.parse(body) : body, { authInfo });
+      await this.handleMessage(typeof body === 'string' ? JSON.parse(body) : body, { requestInfo, authInfo });
     } catch {
       res.writeHead(400).end(`Invalid message: ${body}`);
       return;
@@ -118,7 +182,7 @@ export class SSEServerTransport implements Transport {
   /**
    * Handle a client message, regardless of how it arrived. This can be used to inform the server of messages that arrive via a means different than HTTP POST.
    */
-  async handleMessage(message: unknown, extra?: { authInfo?: AuthInfo }): Promise<void> {
+  async handleMessage(message: unknown, extra?: MessageExtraInfo): Promise<void> {
     let parsedMessage: JSONRPCMessage;
     try {
       parsedMessage = JSONRPCMessageSchema.parse(message);
diff --git a/src/server/streamableHttp.test.ts b/src/server/streamableHttp.test.ts
index b961f6c..502435e 100644
--- a/src/server/streamableHttp.test.ts
+++ b/src/server/streamableHttp.test.ts
@@ -1,5 +1,5 @@
 import { createServer, type Server, IncomingMessage, ServerResponse } from "node:http";
-import { AddressInfo } from "node:net";
+import { createServer as netCreateServer, AddressInfo } from "node:net";
 import { randomUUID } from "node:crypto";
 import { EventStore, StreamableHTTPServerTransport, EventId, StreamId } from "./streamableHttp.js";
 import { McpServer } from "./mcp.js";
@@ -7,6 +7,20 @@ import { CallToolResult, JSONRPCMessage } from "../types.js";
 import { z } from "zod";
 import { AuthInfo } from "./auth/types.js";
 
+async function getFreePort() {
+  return new Promise(res => {
+    const srv = netCreateServer();
+    srv.listen(0, () => {
+      const address = srv.address()!
+      if (typeof address === "string") {
+        throw new Error("Unexpected address type: " + typeof address);
+      }
+      const port = (address as AddressInfo).port;
+      srv.close((_err) => res(port))
+    });
+  })
+}
+
 /**
  * Test server configuration for StreamableHTTPServerTransport tests
  */
@@ -185,6 +199,8 @@ async function sendPostRequest(baseUrl: URL, message: JSONRPCMessage | JSONRPCMe
 
   if (sessionId) {
     headers["mcp-session-id"] = sessionId;
+    // After initialization, include the protocol version header
+    headers["mcp-protocol-version"] = "2025-03-26";
   }
 
   return fetch(baseUrl, {
@@ -206,6 +222,7 @@ function expectErrorResponse(data: unknown, expectedCode: number, expectedMessag
 
 describe("StreamableHTTPServerTransport", () => {
   let server: Server;
+  let mcpServer: McpServer;
   let transport: StreamableHTTPServerTransport;
   let baseUrl: URL;
   let sessionId: string;
@@ -214,6 +231,7 @@ describe("StreamableHTTPServerTransport", () => {
     const result = await createTestServer();
     server = result.server;
     transport = result.transport;
+    mcpServer = result.mcpServer;
     baseUrl = result.baseUrl;
   });
 
@@ -277,7 +295,7 @@ describe("StreamableHTTPServerTransport", () => {
     expectErrorResponse(errorData, -32600, /Only one initialization request is allowed/);
   });
 
-  it("should pandle post requests via sse response correctly", async () => {
+  it("should handle post requests via sse response correctly", async () => {
     sessionId = await initializeServer();
 
     const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList, sessionId);
@@ -345,6 +363,69 @@ describe("StreamableHTTPServerTransport", () => {
     });
   });
 
+  /***
+   * Test: Tool With Request Info
+   */
+  it("should pass request info to tool callback", async () => {
+    sessionId = await initializeServer();
+
+    mcpServer.tool(
+      "test-request-info",
+      "A simple test tool with request info",
+      { name: z.string().describe("Name to greet") },
+      async ({ name }, { requestInfo }): Promise<CallToolResult> => {
+        return { content: [{ type: "text", text: `Hello, ${name}!` }, { type: "text", text: `${JSON.stringify(requestInfo)}` }] };
+      }
+    );
+
+    const toolCallMessage: JSONRPCMessage = {
+      jsonrpc: "2.0",
+      method: "tools/call",
+      params: {
+        name: "test-request-info",
+        arguments: {
+          name: "Test User",
+        },
+      },
+      id: "call-1",
+    };
+
+    const response = await sendPostRequest(baseUrl, toolCallMessage, sessionId);
+    expect(response.status).toBe(200);
+
+    const text = await readSSEEvent(response);
+    const eventLines = text.split("\n");
+    const dataLine = eventLines.find(line => line.startsWith("data:"));
+    expect(dataLine).toBeDefined();
+
+    const eventData = JSON.parse(dataLine!.substring(5));
+
+    expect(eventData).toMatchObject({
+      jsonrpc: "2.0",
+      result: {
+        content: [
+          { type: "text", text: "Hello, Test User!" },
+          { type: "text", text: expect.any(String) }
+        ],
+      },
+      id: "call-1",
+    });
+
+    const requestInfo = JSON.parse(eventData.result.content[1].text);
+    expect(requestInfo).toMatchObject({
+      headers: {
+        'content-type': 'application/json',
+        accept: 'application/json, text/event-stream',
+        connection: 'keep-alive',
+        'mcp-session-id': sessionId,
+        'accept-language': '*',
+        'user-agent': expect.any(String),
+        'accept-encoding': expect.any(String),
+        'content-length': expect.any(String),
+      },
+    });
+  });
+
   it("should reject requests without a valid session ID", async () => {
     const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList);
 
@@ -376,6 +457,7 @@ describe("StreamableHTTPServerTransport", () => {
       headers: {
         Accept: "text/event-stream",
         "mcp-session-id": sessionId,
+        "mcp-protocol-version": "2025-03-26",
       },
     });
 
@@ -417,6 +499,7 @@ describe("StreamableHTTPServerTransport", () => {
       headers: {
         Accept: "text/event-stream",
         "mcp-session-id": sessionId,
+        "mcp-protocol-version": "2025-03-26",
       },
     });
 
@@ -448,6 +531,7 @@ describe("StreamableHTTPServerTransport", () => {
       headers: {
         Accept: "text/event-stream",
         "mcp-session-id": sessionId,
+        "mcp-protocol-version": "2025-03-26",
       },
     });
 
@@ -459,6 +543,7 @@ describe("StreamableHTTPServerTransport", () => {
       headers: {
         Accept: "text/event-stream",
         "mcp-session-id": sessionId,
+        "mcp-protocol-version": "2025-03-26",
       },
     });
 
@@ -477,6 +562,7 @@ describe("StreamableHTTPServerTransport", () => {
       headers: {
         Accept: "application/json",
         "mcp-session-id": sessionId,
+        "mcp-protocol-version": "2025-03-26",
       },
     });
 
@@ -670,6 +756,7 @@ describe("StreamableHTTPServerTransport", () => {
       headers: {
         Accept: "text/event-stream",
         "mcp-session-id": sessionId,
+        "mcp-protocol-version": "2025-03-26",
       },
     });
 
@@ -705,7 +792,10 @@ describe("StreamableHTTPServerTransport", () => {
     // Now DELETE the session
     const deleteResponse = await fetch(tempUrl, {
       method: "DELETE",
-      headers: { "mcp-session-id": tempSessionId || "" },
+      headers: {
+        "mcp-session-id": tempSessionId || "",
+        "mcp-protocol-version": "2025-03-26",
+      },
     });
 
     expect(deleteResponse.status).toBe(200);
@@ -721,13 +811,124 @@ describe("StreamableHTTPServerTransport", () => {
     // Try to delete with invalid session ID
     const response = await fetch(baseUrl, {
       method: "DELETE",
-      headers: { "mcp-session-id": "invalid-session-id" },
+      headers: {
+        "mcp-session-id": "invalid-session-id",
+        "mcp-protocol-version": "2025-03-26",
+      },
     });
 
     expect(response.status).toBe(404);
     const errorData = await response.json();
     expectErrorResponse(errorData, -32001, /Session not found/);
   });
+
+  describe("protocol version header validation", () => {
+    it("should accept requests with matching protocol version", async () => {
+      sessionId = await initializeServer();
+
+      // Send request with matching protocol version
+      const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList, sessionId);
+
+      expect(response.status).toBe(200);
+    });
+
+    it("should accept requests without protocol version header", async () => {
+      sessionId = await initializeServer();
+
+      // Send request without protocol version header
+      const response = await fetch(baseUrl, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+          Accept: "application/json, text/event-stream",
+          "mcp-session-id": sessionId,
+          // No mcp-protocol-version header
+        },
+        body: JSON.stringify(TEST_MESSAGES.toolsList),
+      });
+
+      expect(response.status).toBe(200);
+    });
+
+    it("should reject requests with unsupported protocol version", async () => {
+      sessionId = await initializeServer();
+
+      // Send request with unsupported protocol version
+      const response = await fetch(baseUrl, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+          Accept: "application/json, text/event-stream",
+          "mcp-session-id": sessionId,
+          "mcp-protocol-version": "1999-01-01", // Unsupported version
+        },
+        body: JSON.stringify(TEST_MESSAGES.toolsList),
+      });
+
+      expect(response.status).toBe(400);
+      const errorData = await response.json();
+      expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version \(supported versions: .+\)/);
+    });
+
+    it("should accept when protocol version differs from negotiated version", async () => {
+      sessionId = await initializeServer();
+
+      // Spy on console.warn to verify warning is logged
+      const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
+
+      // Send request with different but supported protocol version
+      const response = await fetch(baseUrl, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+          Accept: "application/json, text/event-stream",
+          "mcp-session-id": sessionId,
+          "mcp-protocol-version": "2024-11-05", // Different but supported version
+        },
+        body: JSON.stringify(TEST_MESSAGES.toolsList),
+      });
+
+      // Request should still succeed
+      expect(response.status).toBe(200);
+
+      warnSpy.mockRestore();
+    });
+
+    it("should handle protocol version validation for GET requests", async () => {
+      sessionId = await initializeServer();
+
+      // GET request with unsupported protocol version
+      const response = await fetch(baseUrl, {
+        method: "GET",
+        headers: {
+          Accept: "text/event-stream",
+          "mcp-session-id": sessionId,
+          "mcp-protocol-version": "invalid-version",
+        },
+      });
+
+      expect(response.status).toBe(400);
+      const errorData = await response.json();
+      expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version \(supported versions: .+\)/);
+    });
+
+    it("should handle protocol version validation for DELETE requests", async () => {
+      sessionId = await initializeServer();
+
+      // DELETE request with unsupported protocol version
+      const response = await fetch(baseUrl, {
+        method: "DELETE",
+        headers: {
+          "mcp-session-id": sessionId,
+          "mcp-protocol-version": "invalid-version",
+        },
+      });
+
+      expect(response.status).toBe(400);
+      const errorData = await response.json();
+      expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version \(supported versions: .+\)/);
+    });
+  });
 });
 
 describe("StreamableHTTPServerTransport with AuthInfo", () => {
@@ -764,12 +965,12 @@ describe("StreamableHTTPServerTransport with AuthInfo", () => {
       method: "tools/call",
       params: {
         name: "profile",
-        arguments: {active: true},
+        arguments: { active: true },
       },
       id: "call-1",
     };
 
-    const response = await sendPostRequest(baseUrl, toolCallMessage, sessionId, {'authorization': 'Bearer test-token'});
+    const response = await sendPostRequest(baseUrl, toolCallMessage, sessionId, { 'authorization': 'Bearer test-token' });
     expect(response.status).toBe(200);
 
     const text = await readSSEEvent(response);
@@ -791,7 +992,7 @@ describe("StreamableHTTPServerTransport with AuthInfo", () => {
       id: "call-1",
     });
   });
-  
+
   it("should calls tool without authInfo when it is optional", async () => {
     sessionId = await initializeServer();
 
@@ -800,7 +1001,7 @@ describe("StreamableHTTPServerTransport with AuthInfo", () => {
       method: "tools/call",
       params: {
         name: "profile",
-        arguments: {active: false},
+        arguments: { active: false },
       },
       id: "call-1",
     };
@@ -1120,6 +1321,7 @@ describe("StreamableHTTPServerTransport with resumability", () => {
       headers: {
         Accept: "text/event-stream",
         "mcp-session-id": sessionId,
+        "mcp-protocol-version": "2025-03-26",
       },
     });
 
@@ -1196,6 +1398,7 @@ describe("StreamableHTTPServerTransport with resumability", () => {
       headers: {
         Accept: "text/event-stream",
         "mcp-session-id": sessionId,
+        "mcp-protocol-version": "2025-03-26",
         "last-event-id": firstEventId
       },
     });
@@ -1282,15 +1485,292 @@ describe("StreamableHTTPServerTransport in stateless mode", () => {
     // Open first SSE stream
     const stream1 = await fetch(baseUrl, {
       method: "GET",
-      headers: { Accept: "text/event-stream" },
+      headers: {
+        Accept: "text/event-stream",
+        "mcp-protocol-version": "2025-03-26"
+      },
     });
     expect(stream1.status).toBe(200);
 
     // Open second SSE stream - should still be rejected, stateless mode still only allows one
     const stream2 = await fetch(baseUrl, {
       method: "GET",
-      headers: { Accept: "text/event-stream" },
+      headers: {
+        Accept: "text/event-stream",
+        "mcp-protocol-version": "2025-03-26"
+      },
     });
     expect(stream2.status).toBe(409); // Conflict - only one stream allowed
   });
-});
\ No newline at end of file
+});
+
+// Test DNS rebinding protection
+describe("StreamableHTTPServerTransport DNS rebinding protection", () => {
+  let server: Server;
+  let transport: StreamableHTTPServerTransport;
+  let baseUrl: URL;
+
+  afterEach(async () => {
+    if (server && transport) {
+      await stopTestServer({ server, transport });
+    }
+  });
+
+  describe("Host header validation", () => {
+    it("should accept requests with allowed host headers", async () => {
+      const result = await createTestServerWithDnsProtection({
+        sessionIdGenerator: undefined,
+        allowedHosts: ['localhost'],
+        enableDnsRebindingProtection: true,
+      });
+      server = result.server;
+      transport = result.transport;
+      baseUrl = result.baseUrl;
+
+      // Note: fetch() automatically sets Host header to match the URL
+      // Since we're connecting to localhost:3001 and that's in allowedHosts, this should work
+      const response = await fetch(baseUrl, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+          Accept: "application/json, text/event-stream",
+        },
+        body: JSON.stringify(TEST_MESSAGES.initialize),
+      });
+
+      expect(response.status).toBe(200);
+    });
+
+    it("should reject requests with disallowed host headers", async () => {
+      // Test DNS rebinding protection by creating a server that only allows example.com
+      // but we're connecting via localhost, so it should be rejected
+      const result = await createTestServerWithDnsProtection({
+        sessionIdGenerator: undefined,
+        allowedHosts: ['example.com:3001'],
+        enableDnsRebindingProtection: true,
+      });
+      server = result.server;
+      transport = result.transport;
+      baseUrl = result.baseUrl;
+
+      const response = await fetch(baseUrl, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+          Accept: "application/json, text/event-stream",
+        },
+        body: JSON.stringify(TEST_MESSAGES.initialize),
+      });
+
+      expect(response.status).toBe(403);
+      const body = await response.json();
+      expect(body.error.message).toContain("Invalid Host header:");
+    });
+
+    it("should reject GET requests with disallowed host headers", async () => {
+      const result = await createTestServerWithDnsProtection({
+        sessionIdGenerator: undefined,
+        allowedHosts: ['example.com:3001'],
+        enableDnsRebindingProtection: true,
+      });
+      server = result.server;
+      transport = result.transport;
+      baseUrl = result.baseUrl;
+
+      const response = await fetch(baseUrl, {
+        method: "GET",
+        headers: {
+          Accept: "text/event-stream",
+        },
+      });
+
+      expect(response.status).toBe(403);
+    });
+  });
+
+  describe("Origin header validation", () => {
+    it("should accept requests with allowed origin headers", async () => {
+      const result = await createTestServerWithDnsProtection({
+        sessionIdGenerator: undefined,
+        allowedOrigins: ['http://localhost:3000', 'https://example.com'],
+        enableDnsRebindingProtection: true,
+      });
+      server = result.server;
+      transport = result.transport;
+      baseUrl = result.baseUrl;
+
+      const response = await fetch(baseUrl, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+          Accept: "application/json, text/event-stream",
+          Origin: "http://localhost:3000",
+        },
+        body: JSON.stringify(TEST_MESSAGES.initialize),
+      });
+
+      expect(response.status).toBe(200);
+    });
+
+    it("should reject requests with disallowed origin headers", async () => {
+      const result = await createTestServerWithDnsProtection({
+        sessionIdGenerator: undefined,
+        allowedOrigins: ['http://localhost:3000'],
+        enableDnsRebindingProtection: true,
+      });
+      server = result.server;
+      transport = result.transport;
+      baseUrl = result.baseUrl;
+
+      const response = await fetch(baseUrl, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+          Accept: "application/json, text/event-stream",
+          Origin: "http://evil.com",
+        },
+        body: JSON.stringify(TEST_MESSAGES.initialize),
+      });
+
+      expect(response.status).toBe(403);
+      const body = await response.json();
+      expect(body.error.message).toBe("Invalid Origin header: http://evil.com");
+    });
+  });
+
+  describe("enableDnsRebindingProtection option", () => {
+    it("should skip all validations when enableDnsRebindingProtection is false", async () => {
+      const result = await createTestServerWithDnsProtection({
+        sessionIdGenerator: undefined,
+        allowedHosts: ['localhost'],
+        allowedOrigins: ['http://localhost:3000'],
+        enableDnsRebindingProtection: false,
+      });
+      server = result.server;
+      transport = result.transport;
+      baseUrl = result.baseUrl;
+
+      const response = await fetch(baseUrl, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+          Accept: "application/json, text/event-stream",
+          Host: "evil.com",
+          Origin: "http://evil.com",
+        },
+        body: JSON.stringify(TEST_MESSAGES.initialize),
+      });
+
+      // Should pass even with invalid headers because protection is disabled
+      expect(response.status).toBe(200);
+    });
+  });
+
+  describe("Combined validations", () => {
+    it("should validate both host and origin when both are configured", async () => {
+      const result = await createTestServerWithDnsProtection({
+        sessionIdGenerator: undefined,
+        allowedHosts: ['localhost'],
+        allowedOrigins: ['http://localhost:3001'],
+        enableDnsRebindingProtection: true,
+      });
+      server = result.server;
+      transport = result.transport;
+      baseUrl = result.baseUrl;
+
+      // Test with invalid origin (host will be automatically correct via fetch)
+      const response1 = await fetch(baseUrl, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+          Accept: "application/json, text/event-stream",
+          Origin: "http://evil.com",
+        },
+        body: JSON.stringify(TEST_MESSAGES.initialize),
+      });
+
+      expect(response1.status).toBe(403);
+      const body1 = await response1.json();
+      expect(body1.error.message).toBe("Invalid Origin header: http://evil.com");
+
+      // Test with valid origin
+      const response2 = await fetch(baseUrl, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+          Accept: "application/json, text/event-stream",
+          Origin: "http://localhost:3001",
+        },
+        body: JSON.stringify(TEST_MESSAGES.initialize),
+      });
+
+      expect(response2.status).toBe(200);
+    });
+  });
+});
+
+/**
+ * Helper to create test server with DNS rebinding protection options
+ */
+async function createTestServerWithDnsProtection(config: {
+  sessionIdGenerator: (() => string) | undefined;
+  allowedHosts?: string[];
+  allowedOrigins?: string[];
+  enableDnsRebindingProtection?: boolean;
+}): Promise<{
+  server: Server;
+  transport: StreamableHTTPServerTransport;
+  mcpServer: McpServer;
+  baseUrl: URL;
+}> {
+  const mcpServer = new McpServer(
+    { name: "test-server", version: "1.0.0" },
+    { capabilities: { logging: {} } }
+  );
+
+  const port = await getFreePort();
+
+  if (config.allowedHosts) {
+    config.allowedHosts = config.allowedHosts.map(host => {
+      if (host.includes(':')) {
+        return host;
+      }
+      return `localhost:${port}`;
+    });
+  }
+
+  const transport = new StreamableHTTPServerTransport({
+    sessionIdGenerator: config.sessionIdGenerator,
+    allowedHosts: config.allowedHosts,
+    allowedOrigins: config.allowedOrigins,
+    enableDnsRebindingProtection: config.enableDnsRebindingProtection,
+  });
+
+  await mcpServer.connect(transport);
+
+  const httpServer = createServer(async (req, res) => {
+    if (req.method === "POST") {
+      let body = "";
+      req.on("data", (chunk) => (body += chunk));
+      req.on("end", async () => {
+        const parsedBody = JSON.parse(body);
+        await transport.handleRequest(req as IncomingMessage & { auth?: AuthInfo }, res, parsedBody);
+      });
+    } else {
+      await transport.handleRequest(req as IncomingMessage & { auth?: AuthInfo }, res);
+    }
+  });
+
+  await new Promise<void>((resolve) => {
+    httpServer.listen(port, () => resolve());
+  });
+
+  const serverUrl = new URL(`http://localhost:${port}/`);
+
+  return {
+    server: httpServer,
+    transport,
+    mcpServer,
+    baseUrl: serverUrl,
+  };
+}
\ No newline at end of file
diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts
index dc99c30..022d1a4 100644
--- a/src/server/streamableHttp.ts
+++ b/src/server/streamableHttp.ts
@@ -1,6 +1,6 @@
 import { IncomingMessage, ServerResponse } from "node:http";
 import { Transport } from "../shared/transport.js";
-import { isInitializeRequest, isJSONRPCError, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema, RequestId } from "../types.js";
+import { MessageExtraInfo, RequestInfo, isInitializeRequest, isJSONRPCError, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema, RequestId, SUPPORTED_PROTOCOL_VERSIONS, DEFAULT_NEGOTIATED_PROTOCOL_VERSION } from "../types.js";
 import getRawBody from "raw-body";
 import contentType from "content-type";
 import { randomUUID } from "node:crypto";
@@ -61,6 +61,24 @@ export interface StreamableHTTPServerTransportOptions {
    * If provided, resumability will be enabled, allowing clients to reconnect and resume messages
    */
   eventStore?: EventStore;
+
+  /**
+   * List of allowed host header values for DNS rebinding protection.
+   * If not specified, host validation is disabled.
+   */
+  allowedHosts?: string[];
+  
+  /**
+   * List of allowed origin header values for DNS rebinding protection.
+   * If not specified, origin validation is disabled.
+   */
+  allowedOrigins?: string[];
+  
+  /**
+   * Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured).
+   * Default is false for backwards compatibility.
+   */
+  enableDnsRebindingProtection?: boolean;
 }
 
 /**
@@ -109,17 +127,23 @@ export class StreamableHTTPServerTransport implements Transport {
   private _standaloneSseStreamId: string = '_GET_stream';
   private _eventStore?: EventStore;
   private _onsessioninitialized?: (sessionId: string) => void;
+  private _allowedHosts?: string[];
+  private _allowedOrigins?: string[];
+  private _enableDnsRebindingProtection: boolean;
 
-  sessionId?: string | undefined;
+  sessionId?: string;
   onclose?: () => void;
   onerror?: (error: Error) => void;
-  onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void;
+  onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void;
 
   constructor(options: StreamableHTTPServerTransportOptions) {
     this.sessionIdGenerator = options.sessionIdGenerator;
     this._enableJsonResponse = options.enableJsonResponse ?? false;
     this._eventStore = options.eventStore;
     this._onsessioninitialized = options.onsessioninitialized;
+    this._allowedHosts = options.allowedHosts;
+    this._allowedOrigins = options.allowedOrigins;
+    this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false;
   }
 
   /**
@@ -133,10 +157,54 @@ export class StreamableHTTPServerTransport implements Transport {
     this._started = true;
   }
 
+  /**
+   * Validates request headers for DNS rebinding protection.
+   * @returns Error message if validation fails, undefined if validation passes.
+   */
+  private validateRequestHeaders(req: IncomingMessage): string | undefined {
+    // Skip validation if protection is not enabled
+    if (!this._enableDnsRebindingProtection) {
+      return undefined;
+    }
+
+    // Validate Host header if allowedHosts is configured
+    if (this._allowedHosts && this._allowedHosts.length > 0) {
+      const hostHeader = req.headers.host;
+      if (!hostHeader || !this._allowedHosts.includes(hostHeader)) {
+        return `Invalid Host header: ${hostHeader}`;
+      }
+    }
+
+    // Validate Origin header if allowedOrigins is configured
+    if (this._allowedOrigins && this._allowedOrigins.length > 0) {
+      const originHeader = req.headers.origin;
+      if (!originHeader || !this._allowedOrigins.includes(originHeader)) {
+        return `Invalid Origin header: ${originHeader}`;
+      }
+    }
+
+    return undefined;
+  }
+
   /**
    * Handles an incoming HTTP request, whether GET or POST
    */
   async handleRequest(req: IncomingMessage & { auth?: AuthInfo }, res: ServerResponse, parsedBody?: unknown): Promise<void> {
+    // Validate request headers for DNS rebinding protection
+    const validationError = this.validateRequestHeaders(req);
+    if (validationError) {
+      res.writeHead(403).end(JSON.stringify({
+        jsonrpc: "2.0",
+        error: {
+          code: -32000,
+          message: validationError
+        },
+        id: null
+      }));
+      this.onerror?.(new Error(validationError));
+      return;
+    }
+
     if (req.method === "POST") {
       await this.handlePostRequest(req, res, parsedBody);
     } else if (req.method === "GET") {
@@ -172,6 +240,9 @@ export class StreamableHTTPServerTransport implements Transport {
     if (!this.validateSession(req, res)) {
       return;
     }
+    if (!this.validateProtocolVersion(req, res)) {
+      return;
+    }
     // Handle resumability: check for Last-Event-ID header
     if (this._eventStore) {
       const lastEventId = req.headers['last-event-id'] as string | undefined;
@@ -318,6 +389,7 @@ export class StreamableHTTPServerTransport implements Transport {
       }
 
       const authInfo: AuthInfo | undefined = req.auth;
+      const requestInfo: RequestInfo = { headers: req.headers };
 
       let rawMessage;
       if (parsedBody !== undefined) {
@@ -378,11 +450,17 @@ export class StreamableHTTPServerTransport implements Transport {
         }
 
       }
-      // If an Mcp-Session-Id is returned by the server during initialization,
-      // clients using the Streamable HTTP transport MUST include it 
-      // in the Mcp-Session-Id header on all of their subsequent HTTP requests.
-      if (!isInitializationRequest && !this.validateSession(req, res)) {
-        return;
+      if (!isInitializationRequest) {
+        // If an Mcp-Session-Id is returned by the server during initialization,
+        // clients using the Streamable HTTP transport MUST include it 
+        // in the Mcp-Session-Id header on all of their subsequent HTTP requests.
+        if (!this.validateSession(req, res)) {
+          return;
+        }
+        // Mcp-Protocol-Version header is required for all requests after initialization.
+        if (!this.validateProtocolVersion(req, res)) {
+          return;
+        }
       }
 
 
@@ -395,7 +473,7 @@ export class StreamableHTTPServerTransport implements Transport {
 
         // handle each message
         for (const message of messages) {
-          this.onmessage?.(message, { authInfo });
+          this.onmessage?.(message, { authInfo, requestInfo });
         }
       } else if (hasRequests) {
         // The default behavior is to use SSE streaming
@@ -430,7 +508,7 @@ export class StreamableHTTPServerTransport implements Transport {
 
         // handle each message
         for (const message of messages) {
-          this.onmessage?.(message, { authInfo });
+          this.onmessage?.(message, { authInfo, requestInfo });
         }
         // The server SHOULD NOT close the SSE stream before sending all JSON-RPC responses
         // This will be handled by the send() method when responses are ready
@@ -457,6 +535,9 @@ export class StreamableHTTPServerTransport implements Transport {
     if (!this.validateSession(req, res)) {
       return;
     }
+    if (!this.validateProtocolVersion(req, res)) {
+      return;
+    }
     await this.close();
     res.writeHead(200).end();
   }
@@ -524,6 +605,25 @@ export class StreamableHTTPServerTransport implements Transport {
     return true;
   }
 
+  private validateProtocolVersion(req: IncomingMessage, res: ServerResponse): boolean {
+    let protocolVersion = req.headers["mcp-protocol-version"] ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
+    if (Array.isArray(protocolVersion)) {
+      protocolVersion = protocolVersion[protocolVersion.length - 1];
+    }
+
+    if (!SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
+      res.writeHead(400).end(JSON.stringify({
+        jsonrpc: "2.0",
+        error: {
+          code: -32000,
+          message: `Bad Request: Unsupported protocol version (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")})`
+        },
+        id: null
+      }));
+      return false;
+    }
+    return true;
+  }
 
   async close(): Promise<void> {
     // Close all SSE connections
diff --git a/src/server/title.test.ts b/src/server/title.test.ts
new file mode 100644
index 0000000..3f64570
--- /dev/null
+++ b/src/server/title.test.ts
@@ -0,0 +1,236 @@
+import { Server } from "./index.js";
+import { Client } from "../client/index.js";
+import { InMemoryTransport } from "../inMemory.js";
+import { z } from "zod";
+import { McpServer, ResourceTemplate } from "./mcp.js";
+
+describe("Title field backwards compatibility", () => {
+  it("should work with tools that have title", async () => {
+    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+    const server = new McpServer(
+      { name: "test-server", version: "1.0.0" },
+      { capabilities: {} }
+    );
+
+    // Register tool with title
+    server.registerTool(
+      "test-tool",
+      {
+        title: "Test Tool Display Name",
+        description: "A test tool",
+        inputSchema: {
+          value: z.string()
+        }
+      },
+      async () => ({ content: [{ type: "text", text: "result" }] })
+    );
+
+    const client = new Client({ name: "test-client", version: "1.0.0" });
+
+    await server.server.connect(serverTransport);
+    await client.connect(clientTransport);
+
+    const tools = await client.listTools();
+    expect(tools.tools).toHaveLength(1);
+    expect(tools.tools[0].name).toBe("test-tool");
+    expect(tools.tools[0].title).toBe("Test Tool Display Name");
+    expect(tools.tools[0].description).toBe("A test tool");
+  });
+
+  it("should work with tools without title", async () => {
+    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+    const server = new McpServer(
+      { name: "test-server", version: "1.0.0" },
+      { capabilities: {} }
+    );
+
+    // Register tool without title
+    server.tool(
+      "test-tool",
+      "A test tool",
+      { value: z.string() },
+      async () => ({ content: [{ type: "text", text: "result" }] })
+    );
+
+    const client = new Client({ name: "test-client", version: "1.0.0" });
+
+    await server.server.connect(serverTransport);
+    await client.connect(clientTransport);
+
+    const tools = await client.listTools();
+    expect(tools.tools).toHaveLength(1);
+    expect(tools.tools[0].name).toBe("test-tool");
+    expect(tools.tools[0].title).toBeUndefined();
+    expect(tools.tools[0].description).toBe("A test tool");
+  });
+
+  it("should work with prompts that have title using update", async () => {
+    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+    const server = new McpServer(
+      { name: "test-server", version: "1.0.0" },
+      { capabilities: {} }
+    );
+
+    // Register prompt with title by updating after creation
+    const prompt = server.prompt(
+      "test-prompt",
+      "A test prompt",
+      async () => ({ messages: [{ role: "user", content: { type: "text", text: "test" } }] })
+    );
+    prompt.update({ title: "Test Prompt Display Name" });
+
+    const client = new Client({ name: "test-client", version: "1.0.0" });
+
+    await server.server.connect(serverTransport);
+    await client.connect(clientTransport);
+
+    const prompts = await client.listPrompts();
+    expect(prompts.prompts).toHaveLength(1);
+    expect(prompts.prompts[0].name).toBe("test-prompt");
+    expect(prompts.prompts[0].title).toBe("Test Prompt Display Name");
+    expect(prompts.prompts[0].description).toBe("A test prompt");
+  });
+
+  it("should work with prompts using registerPrompt", async () => {
+    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+    const server = new McpServer(
+      { name: "test-server", version: "1.0.0" },
+      { capabilities: {} }
+    );
+
+    // Register prompt with title using registerPrompt
+    server.registerPrompt(
+      "test-prompt",
+      {
+        title: "Test Prompt Display Name",
+        description: "A test prompt",
+        argsSchema: { input: z.string() }
+      },
+      async ({ input }) => ({
+        messages: [{
+          role: "user",
+          content: { type: "text", text: `test: ${input}` }
+        }]
+      })
+    );
+
+    const client = new Client({ name: "test-client", version: "1.0.0" });
+
+    await server.server.connect(serverTransport);
+    await client.connect(clientTransport);
+
+    const prompts = await client.listPrompts();
+    expect(prompts.prompts).toHaveLength(1);
+    expect(prompts.prompts[0].name).toBe("test-prompt");
+    expect(prompts.prompts[0].title).toBe("Test Prompt Display Name");
+    expect(prompts.prompts[0].description).toBe("A test prompt");
+    expect(prompts.prompts[0].arguments).toHaveLength(1);
+  });
+
+  it("should work with resources using registerResource", async () => {
+    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+    const server = new McpServer(
+      { name: "test-server", version: "1.0.0" },
+      { capabilities: {} }
+    );
+
+    // Register resource with title using registerResource
+    server.registerResource(
+      "test-resource",
+      "https://example.com/test",
+      {
+        title: "Test Resource Display Name",
+        description: "A test resource",
+        mimeType: "text/plain"
+      },
+      async () => ({
+        contents: [{
+          uri: "https://example.com/test",
+          text: "test content"
+        }]
+      })
+    );
+
+    const client = new Client({ name: "test-client", version: "1.0.0" });
+
+    await server.server.connect(serverTransport);
+    await client.connect(clientTransport);
+
+    const resources = await client.listResources();
+    expect(resources.resources).toHaveLength(1);
+    expect(resources.resources[0].name).toBe("test-resource");
+    expect(resources.resources[0].title).toBe("Test Resource Display Name");
+    expect(resources.resources[0].description).toBe("A test resource");
+    expect(resources.resources[0].mimeType).toBe("text/plain");
+  });
+
+  it("should work with dynamic resources using registerResource", async () => {
+    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+    const server = new McpServer(
+      { name: "test-server", version: "1.0.0" },
+      { capabilities: {} }
+    );
+
+    // Register dynamic resource with title using registerResource
+    server.registerResource(
+      "user-profile",
+      new ResourceTemplate("users://{userId}/profile", { list: undefined }),
+      {
+        title: "User Profile",
+        description: "User profile information"
+      },
+      async (uri, { userId }, _extra) => ({
+        contents: [{
+          uri: uri.href,
+          text: `Profile data for user ${userId}`
+        }]
+      })
+    );
+
+    const client = new Client({ name: "test-client", version: "1.0.0" });
+
+    await server.server.connect(serverTransport);
+    await client.connect(clientTransport);
+
+    const resourceTemplates = await client.listResourceTemplates();
+    expect(resourceTemplates.resourceTemplates).toHaveLength(1);
+    expect(resourceTemplates.resourceTemplates[0].name).toBe("user-profile");
+    expect(resourceTemplates.resourceTemplates[0].title).toBe("User Profile");
+    expect(resourceTemplates.resourceTemplates[0].description).toBe("User profile information");
+    expect(resourceTemplates.resourceTemplates[0].uriTemplate).toBe("users://{userId}/profile");
+
+    // Test reading the resource
+    const readResult = await client.readResource({ uri: "users://123/profile" });
+    expect(readResult.contents).toHaveLength(1);
+    expect(readResult.contents[0].text).toBe("Profile data for user 123");
+  });
+
+  it("should support serverInfo with title", async () => {
+    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+
+    const server = new Server(
+      {
+        name: "test-server",
+        version: "1.0.0",
+        title: "Test Server Display Name"
+      },
+      { capabilities: {} }
+    );
+
+    const client = new Client({ name: "test-client", version: "1.0.0" });
+
+    await server.connect(serverTransport);
+    await client.connect(clientTransport);
+
+    const serverInfo = client.getServerVersion();
+    expect(serverInfo?.name).toBe("test-server");
+    expect(serverInfo?.version).toBe("1.0.0");
+    expect(serverInfo?.title).toBe("Test Server Display Name");
+  });
+});
\ No newline at end of file
diff --git a/src/shared/auth-utils.test.ts b/src/shared/auth-utils.test.ts
new file mode 100644
index 0000000..c1fa7bd
--- /dev/null
+++ b/src/shared/auth-utils.test.ts
@@ -0,0 +1,61 @@
+import { resourceUrlFromServerUrl, checkResourceAllowed } from './auth-utils.js';
+
+describe('auth-utils', () => {
+  describe('resourceUrlFromServerUrl', () => {
+    it('should remove fragments', () => {
+      expect(resourceUrlFromServerUrl(new URL('https://example.com/path#fragment')).href).toBe('https://example.com/path');
+      expect(resourceUrlFromServerUrl(new URL('https://example.com#fragment')).href).toBe('https://example.com/');
+      expect(resourceUrlFromServerUrl(new URL('https://example.com/path?query=1#fragment')).href).toBe('https://example.com/path?query=1');
+    });
+
+    it('should return URL unchanged if no fragment', () => {
+      expect(resourceUrlFromServerUrl(new URL('https://example.com')).href).toBe('https://example.com/');
+      expect(resourceUrlFromServerUrl(new URL('https://example.com/path')).href).toBe('https://example.com/path');
+      expect(resourceUrlFromServerUrl(new URL('https://example.com/path?query=1')).href).toBe('https://example.com/path?query=1');
+    });
+
+    it('should keep everything else unchanged', () => {
+      // Case sensitivity preserved
+      expect(resourceUrlFromServerUrl(new URL('https://EXAMPLE.COM/PATH')).href).toBe('https://example.com/PATH');
+      // Ports preserved
+      expect(resourceUrlFromServerUrl(new URL('https://example.com:443/path')).href).toBe('https://example.com/path');
+      expect(resourceUrlFromServerUrl(new URL('https://example.com:8080/path')).href).toBe('https://example.com:8080/path');
+      // Query parameters preserved
+      expect(resourceUrlFromServerUrl(new URL('https://example.com?foo=bar&baz=qux')).href).toBe('https://example.com/?foo=bar&baz=qux');
+      // Trailing slashes preserved
+      expect(resourceUrlFromServerUrl(new URL('https://example.com/')).href).toBe('https://example.com/');
+      expect(resourceUrlFromServerUrl(new URL('https://example.com/path/')).href).toBe('https://example.com/path/');
+    });
+  });
+
+  describe('resourceMatches', () => {
+    it('should match identical URLs', () => {
+      expect(checkResourceAllowed({ requestedResource: 'https://example.com/path', configuredResource: 'https://example.com/path' })).toBe(true);
+      expect(checkResourceAllowed({ requestedResource: 'https://example.com/', configuredResource: 'https://example.com/' })).toBe(true);
+    });
+
+    it('should not match URLs with different paths', () => {
+      expect(checkResourceAllowed({ requestedResource: 'https://example.com/path1', configuredResource: 'https://example.com/path2' })).toBe(false);
+      expect(checkResourceAllowed({ requestedResource: 'https://example.com/', configuredResource: 'https://example.com/path' })).toBe(false);
+    });
+
+    it('should not match URLs with different domains', () => {
+      expect(checkResourceAllowed({ requestedResource: 'https://example.com/path', configuredResource: 'https://example.org/path' })).toBe(false);
+    });
+
+    it('should not match URLs with different ports', () => {
+      expect(checkResourceAllowed({ requestedResource: 'https://example.com:8080/path', configuredResource: 'https://example.com/path' })).toBe(false);
+    });
+
+    it('should not match URLs where one path is a sub-path of another', () => {
+      expect(checkResourceAllowed({ requestedResource: 'https://example.com/mcpxxxx', configuredResource: 'https://example.com/mcp' })).toBe(false);
+      expect(checkResourceAllowed({ requestedResource: 'https://example.com/folder', configuredResource: 'https://example.com/folder/subfolder' })).toBe(false);
+      expect(checkResourceAllowed({ requestedResource: 'https://example.com/api/v1', configuredResource: 'https://example.com/api' })).toBe(true);
+    });
+
+    it('should handle trailing slashes vs no trailing slashes', () => {
+      expect(checkResourceAllowed({ requestedResource: 'https://example.com/mcp/', configuredResource: 'https://example.com/mcp' })).toBe(true);
+      expect(checkResourceAllowed({ requestedResource: 'https://example.com/folder', configuredResource: 'https://example.com/folder/' })).toBe(false);
+    });
+  });
+});
diff --git a/src/shared/auth-utils.ts b/src/shared/auth-utils.ts
new file mode 100644
index 0000000..97a77c0
--- /dev/null
+++ b/src/shared/auth-utils.ts
@@ -0,0 +1,54 @@
+/**
+ * Utilities for handling OAuth resource URIs.
+ */
+
+/**
+ * Converts a server URL to a resource URL by removing the fragment.
+ * RFC 8707 section 2 states that resource URIs "MUST NOT include a fragment component".
+ * Keeps everything else unchanged (scheme, domain, port, path, query).
+ */
+export function resourceUrlFromServerUrl(url: URL | string ): URL {
+  const resourceURL = typeof url === "string" ? new URL(url) : new URL(url.href);
+  resourceURL.hash = ''; // Remove fragment
+  return resourceURL;
+}
+
+/**
+ * Checks if a requested resource URL matches a configured resource URL.
+ * A requested resource matches if it has the same scheme, domain, port,
+ * and its path starts with the configured resource's path.
+ *
+ * @param requestedResource The resource URL being requested
+ * @param configuredResource The resource URL that has been configured
+ * @returns true if the requested resource matches the configured resource, false otherwise
+ */
+ export function checkResourceAllowed(
+   { requestedResource, configuredResource }: {
+     requestedResource: URL | string;
+     configuredResource: URL | string
+   }
+ ): boolean {
+   const requested = typeof requestedResource === "string" ? new URL(requestedResource) : new URL(requestedResource.href);
+   const configured = typeof configuredResource === "string" ? new URL(configuredResource) : new URL(configuredResource.href);
+
+   // Compare the origin (scheme, domain, and port)
+   if (requested.origin !== configured.origin) {
+     return false;
+   }
+
+   // Handle cases like requested=/foo and configured=/foo/
+   if (requested.pathname.length < configured.pathname.length) {
+     return false
+   }
+
+   // Check if the requested path starts with the configured path
+   // Ensure both paths end with / for proper comparison
+   // This ensures that if we have paths like "/api" and "/api/users",
+   // we properly detect that "/api/users" is a subpath of "/api"
+   // By adding a trailing slash if missing, we avoid false positives
+   // where paths like "/api123" would incorrectly match "/api"
+   const requestedPath = requested.pathname.endsWith('/') ? requested.pathname : requested.pathname + '/';
+   const configuredPath = configured.pathname.endsWith('/') ? configured.pathname : configured.pathname + '/';
+
+   return requestedPath.startsWith(configuredPath);
+ }
diff --git a/src/shared/auth.ts b/src/shared/auth.ts
index 65b800e..b906de3 100644
--- a/src/shared/auth.ts
+++ b/src/shared/auth.ts
@@ -98,6 +98,7 @@ export const OAuthClientMetadataSchema = z.object({
   jwks: z.any().optional(),
   software_id: z.string().optional(),
   software_version: z.string().optional(),
+  software_statement: z.string().optional(),
 }).strip();
 
 /**
diff --git a/src/shared/metadataUtils.ts b/src/shared/metadataUtils.ts
new file mode 100644
index 0000000..0119a66
--- /dev/null
+++ b/src/shared/metadataUtils.ts
@@ -0,0 +1,29 @@
+import { BaseMetadata } from "../types.js";
+
+/**
+ * Utilities for working with BaseMetadata objects.
+ */
+
+/**
+ * Gets the display name for an object with BaseMetadata.
+ * For tools, the precedence is: title → annotations.title → name
+ * For other objects: title → name
+ * This implements the spec requirement: "if no title is provided, name should be used for display purposes"
+ */
+export function getDisplayName(metadata: BaseMetadata): string {
+  // First check for title (not undefined and not empty string)
+  if (metadata.title !== undefined && metadata.title !== '') {
+    return metadata.title;
+  }
+
+  // Then check for annotations.title (only present in Tool objects)
+  if ('annotations' in metadata) {
+    const metadataWithAnnotations = metadata as BaseMetadata & { annotations?: { title?: string } };
+    if (metadataWithAnnotations.annotations?.title) {
+      return metadataWithAnnotations.annotations.title;
+    }
+  }
+
+  // Finally fall back to name
+  return metadata.name;
+}
diff --git a/src/shared/protocol.test.ts b/src/shared/protocol.test.ts
index 05bc8f3..b16db73 100644
--- a/src/shared/protocol.test.ts
+++ b/src/shared/protocol.test.ts
@@ -65,6 +65,22 @@ describe("protocol tests", () => {
     expect(oncloseMock).toHaveBeenCalled();
   });
 
+  test("should not overwrite existing hooks when connecting transports", async () => {
+    const oncloseMock = jest.fn();
+    const onerrorMock = jest.fn();
+    const onmessageMock = jest.fn();
+    transport.onclose = oncloseMock;
+    transport.onerror = onerrorMock;
+    transport.onmessage = onmessageMock;
+    await protocol.connect(transport);
+    transport.onclose();
+    transport.onerror(new Error());
+    transport.onmessage("");
+    expect(oncloseMock).toHaveBeenCalled();
+    expect(onerrorMock).toHaveBeenCalled();
+    expect(onmessageMock).toHaveBeenCalled();
+  });
+
   describe("_meta preservation with onprogress", () => {
     test("should preserve existing _meta when adding progressToken", async () => {
       await protocol.connect(transport);
@@ -465,6 +481,7 @@ describe("mergeCapabilities", () => {
       experimental: {
         feature: true,
       },
+      elicitation: {},
       roots: {
         newProp: true,
       },
@@ -473,6 +490,7 @@ describe("mergeCapabilities", () => {
     const merged = mergeCapabilities(base, additional);
     expect(merged).toEqual({
       sampling: {},
+      elicitation: {},
       roots: {
         listChanged: true,
         newProp: true,
diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts
index a04f26e..50bdcc3 100644
--- a/src/shared/protocol.ts
+++ b/src/shared/protocol.ts
@@ -22,6 +22,8 @@ import {
   Result,
   ServerCapabilities,
   RequestMeta,
+  MessageExtraInfo,
+  RequestInfo,
 } from "../types.js";
 import { Transport, TransportSendOptions } from "./transport.js";
 import { AuthInfo } from "../server/auth/types.js";
@@ -127,6 +129,11 @@ export type RequestHandlerExtra<SendRequestT extends Request,
      */
     requestId: RequestId;
 
+    /**
+     * The original HTTP request.
+     */
+    requestInfo?: RequestInfo;
+
     /**
      * Sends a notification that relates to the current request being handled.
      * 
@@ -279,15 +286,21 @@ export abstract class Protocol<
    */
   async connect(transport: Transport): Promise<void> {
     this._transport = transport;
+    const _onclose = this.transport?.onclose;
     this._transport.onclose = () => {
+      _onclose?.();
       this._onclose();
     };
 
+    const _onerror = this.transport?.onerror;
     this._transport.onerror = (error: Error) => {
+      _onerror?.(error);
       this._onerror(error);
     };
 
+    const _onmessage = this._transport?.onmessage;
     this._transport.onmessage = (message, extra) => {
+      _onmessage?.(message, extra);
       if (isJSONRPCResponse(message) || isJSONRPCError(message)) {
         this._onresponse(message);
       } else if (isJSONRPCRequest(message)) {
@@ -295,7 +308,9 @@ export abstract class Protocol<
       } else if (isJSONRPCNotification(message)) {
         this._onnotification(message);
       } else {
-        this._onerror(new Error(`Unknown message type: ${JSON.stringify(message)}`));
+        this._onerror(
+          new Error(`Unknown message type: ${JSON.stringify(message)}`),
+        );
       }
     };
 
@@ -339,7 +354,7 @@ export abstract class Protocol<
       );
   }
 
-  private _onrequest(request: JSONRPCRequest, extra?: { authInfo?: AuthInfo }): void {
+  private _onrequest(request: JSONRPCRequest, extra?: MessageExtraInfo): void {
     const handler =
       this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler;
 
@@ -375,6 +390,7 @@ export abstract class Protocol<
         this.request(r, resultSchema, { ...options, relatedRequestId: request.id }),
       authInfo: extra?.authInfo,
       requestId: request.id,
+      requestInfo: extra?.requestInfo
     };
 
     // Starting with Promise.resolve() puts any synchronous errors into the monad as well.
diff --git a/src/shared/transport.ts b/src/shared/transport.ts
index fe0a60e..96b291f 100644
--- a/src/shared/transport.ts
+++ b/src/shared/transport.ts
@@ -1,5 +1,4 @@
-import { AuthInfo } from "../server/auth/types.js";
-import { JSONRPCMessage, RequestId } from "../types.js";
+import { JSONRPCMessage, MessageExtraInfo, RequestId } from "../types.js";
 
 /**
  * Options for sending a JSON-RPC message.
@@ -66,13 +65,19 @@ export interface Transport {
   /**
    * Callback for when a message (request or response) is received over the connection.
    * 
-   * Includes the authInfo if the transport is authenticated.
+   * Includes the requestInfo and authInfo if the transport is authenticated.
    * 
+   * The requestInfo can be used to get the original request information (headers, etc.)
    */
-  onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void;
+  onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void;
 
   /**
    * The session ID generated for this connection.
    */
   sessionId?: string;
+
+  /**
+   * Sets the protocol version used for the connection (called when the initialize response is received).
+   */
+  setProtocolVersion?: (version: string) => void;
 }
diff --git a/src/types.test.ts b/src/types.test.ts
index 0fbc003..0aee62a 100644
--- a/src/types.test.ts
+++ b/src/types.test.ts
@@ -1,10 +1,18 @@
-import { LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from "./types.js";
+import {
+    LATEST_PROTOCOL_VERSION,
+    SUPPORTED_PROTOCOL_VERSIONS,
+    ResourceLinkSchema,
+    ContentBlockSchema,
+    PromptMessageSchema,
+    CallToolResultSchema,
+    CompleteRequestSchema
+} from "./types.js";
 
 describe("Types", () => {
 
     test("should have correct latest protocol version", () => {
         expect(LATEST_PROTOCOL_VERSION).toBeDefined();
-        expect(LATEST_PROTOCOL_VERSION).toBe("2025-03-26");
+        expect(LATEST_PROTOCOL_VERSION).toBe("2025-06-18");
     });
     test("should have correct supported protocol versions", () => {
         expect(SUPPORTED_PROTOCOL_VERSIONS).toBeDefined();
@@ -12,6 +20,296 @@ describe("Types", () => {
         expect(SUPPORTED_PROTOCOL_VERSIONS).toContain(LATEST_PROTOCOL_VERSION);
         expect(SUPPORTED_PROTOCOL_VERSIONS).toContain("2024-11-05");
         expect(SUPPORTED_PROTOCOL_VERSIONS).toContain("2024-10-07");
+        expect(SUPPORTED_PROTOCOL_VERSIONS).toContain("2025-03-26");
     });
 
+    describe("ResourceLink", () => {
+        test("should validate a minimal ResourceLink", () => {
+            const resourceLink = {
+                type: "resource_link",
+                uri: "file:///path/to/file.txt",
+                name: "file.txt"
+            };
+
+            const result = ResourceLinkSchema.safeParse(resourceLink);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.type).toBe("resource_link");
+                expect(result.data.uri).toBe("file:///path/to/file.txt");
+                expect(result.data.name).toBe("file.txt");
+            }
+        });
+
+        test("should validate a ResourceLink with all optional fields", () => {
+            const resourceLink = {
+                type: "resource_link",
+                uri: "https://example.com/resource",
+                name: "Example Resource",
+                title: "A comprehensive example resource",
+                description: "This resource demonstrates all fields",
+                mimeType: "text/plain",
+                _meta: { custom: "metadata" }
+            };
+
+            const result = ResourceLinkSchema.safeParse(resourceLink);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.title).toBe("A comprehensive example resource");
+                expect(result.data.description).toBe("This resource demonstrates all fields");
+                expect(result.data.mimeType).toBe("text/plain");
+                expect(result.data._meta).toEqual({ custom: "metadata" });
+            }
+        });
+
+        test("should fail validation for invalid type", () => {
+            const invalidResourceLink = {
+                type: "invalid_type",
+                uri: "file:///path/to/file.txt",
+                name: "file.txt"
+            };
+
+            const result = ResourceLinkSchema.safeParse(invalidResourceLink);
+            expect(result.success).toBe(false);
+        });
+
+        test("should fail validation for missing required fields", () => {
+            const invalidResourceLink = {
+                type: "resource_link",
+                uri: "file:///path/to/file.txt"
+                // missing name
+            };
+
+            const result = ResourceLinkSchema.safeParse(invalidResourceLink);
+            expect(result.success).toBe(false);
+        });
+    });
+
+    describe("ContentBlock", () => {
+        test("should validate text content", () => {
+            const textContent = {
+                type: "text",
+                text: "Hello, world!"
+            };
+
+            const result = ContentBlockSchema.safeParse(textContent);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.type).toBe("text");
+            }
+        });
+
+        test("should validate image content", () => {
+            const imageContent = {
+                type: "image",
+                data: "aGVsbG8=", // base64 encoded "hello"
+                mimeType: "image/png"
+            };
+
+            const result = ContentBlockSchema.safeParse(imageContent);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.type).toBe("image");
+            }
+        });
+
+        test("should validate audio content", () => {
+            const audioContent = {
+                type: "audio",
+                data: "aGVsbG8=", // base64 encoded "hello"
+                mimeType: "audio/mp3"
+            };
+
+            const result = ContentBlockSchema.safeParse(audioContent);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.type).toBe("audio");
+            }
+        });
+
+        test("should validate resource link content", () => {
+            const resourceLink = {
+                type: "resource_link",
+                uri: "file:///path/to/file.txt",
+                name: "file.txt",
+                mimeType: "text/plain"
+            };
+
+            const result = ContentBlockSchema.safeParse(resourceLink);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.type).toBe("resource_link");
+            }
+        });
+
+        test("should validate embedded resource content", () => {
+            const embeddedResource = {
+                type: "resource",
+                resource: {
+                    uri: "file:///path/to/file.txt",
+                    mimeType: "text/plain",
+                    text: "File contents"
+                }
+            };
+
+            const result = ContentBlockSchema.safeParse(embeddedResource);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.type).toBe("resource");
+            }
+        });
+    });
+
+    describe("PromptMessage with ContentBlock", () => {
+        test("should validate prompt message with resource link", () => {
+            const promptMessage = {
+                role: "assistant",
+                content: {
+                    type: "resource_link",
+                    uri: "file:///project/src/main.rs",
+                    name: "main.rs",
+                    description: "Primary application entry point",
+                    mimeType: "text/x-rust"
+                }
+            };
+
+            const result = PromptMessageSchema.safeParse(promptMessage);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.content.type).toBe("resource_link");
+            }
+        });
+    });
+
+    describe("CallToolResult with ContentBlock", () => {
+        test("should validate tool result with resource links", () => {
+            const toolResult = {
+                content: [
+                    {
+                        type: "text",
+                        text: "Found the following files:"
+                    },
+                    {
+                        type: "resource_link",
+                        uri: "file:///project/src/main.rs",
+                        name: "main.rs",
+                        description: "Primary application entry point",
+                        mimeType: "text/x-rust"
+                    },
+                    {
+                        type: "resource_link",
+                        uri: "file:///project/src/lib.rs",
+                        name: "lib.rs",
+                        description: "Library exports",
+                        mimeType: "text/x-rust"
+                    }
+                ]
+            };
+
+            const result = CallToolResultSchema.safeParse(toolResult);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.content).toHaveLength(3);
+                expect(result.data.content[0].type).toBe("text");
+                expect(result.data.content[1].type).toBe("resource_link");
+                expect(result.data.content[2].type).toBe("resource_link");
+            }
+        });
+
+        test("should validate empty content array with default", () => {
+            const toolResult = {};
+
+            const result = CallToolResultSchema.safeParse(toolResult);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.content).toEqual([]);
+            }
+        });
+    });
+
+    describe("CompleteRequest", () => {
+        test("should validate a CompleteRequest without resolved field", () => {
+            const request = {
+                method: "completion/complete",
+                params: {
+                    ref: { type: "ref/prompt", name: "greeting" },
+                    argument: { name: "name", value: "A" }
+                }
+            };
+
+            const result = CompleteRequestSchema.safeParse(request);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.method).toBe("completion/complete");
+                expect(result.data.params.ref.type).toBe("ref/prompt");
+                expect(result.data.params.context).toBeUndefined();
+            }
+        });
+
+        test("should validate a CompleteRequest with resolved field", () => {
+            const request = {
+                method: "completion/complete",
+                params: {
+                    ref: { type: "ref/resource", uri: "github://repos/{owner}/{repo}" },
+                    argument: { name: "repo", value: "t" },
+                    context: {
+                        arguments: {
+                            "{owner}": "microsoft"
+                        }
+                    }
+                }
+            };
+
+            const result = CompleteRequestSchema.safeParse(request);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.params.context?.arguments).toEqual({
+                    "{owner}": "microsoft"
+                });
+            }
+        });
+
+        test("should validate a CompleteRequest with empty resolved field", () => {
+            const request = {
+                method: "completion/complete",
+                params: {
+                    ref: { type: "ref/prompt", name: "test" },
+                    argument: { name: "arg", value: "" },
+                    context: {
+                        arguments: {}
+                    }
+                }
+            };
+
+            const result = CompleteRequestSchema.safeParse(request);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.params.context?.arguments).toEqual({});
+            }
+        });
+
+        test("should validate a CompleteRequest with multiple resolved variables", () => {
+            const request = {
+                method: "completion/complete",
+                params: {
+                    ref: { type: "ref/resource", uri: "api://v1/{tenant}/{resource}/{id}" },
+                    argument: { name: "id", value: "123" },
+                    context: {
+                        arguments: {
+                            "{tenant}": "acme-corp",
+                            "{resource}": "users"
+                        }
+                    }
+                }
+            };
+
+            const result = CompleteRequestSchema.safeParse(request);
+            expect(result.success).toBe(true);
+            if (result.success) {
+                expect(result.data.params.context?.arguments).toEqual({
+                    "{tenant}": "acme-corp",
+                    "{resource}": "users"
+                });
+            }
+        });
+    });
 });
diff --git a/src/types.ts b/src/types.ts
index ae25848..f66d2c4 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,8 +1,11 @@
 import { z, ZodTypeAny } from "zod";
+import { AuthInfo } from "./server/auth/types.js";
 
-export const LATEST_PROTOCOL_VERSION = "2025-03-26";
+export const LATEST_PROTOCOL_VERSION = "2025-06-18";
+export const DEFAULT_NEGOTIATED_PROTOCOL_VERSION = "2025-03-26";
 export const SUPPORTED_PROTOCOL_VERSIONS = [
   LATEST_PROTOCOL_VERSION,
+  "2025-03-26",
   "2024-11-05",
   "2024-10-07",
 ];
@@ -43,7 +46,8 @@ export const RequestSchema = z.object({
 const BaseNotificationParamsSchema = z
   .object({
     /**
-     * This parameter name is reserved by MCP to allow clients and servers to attach additional metadata to their notifications.
+     * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
+     * for notes on _meta usage.
      */
     _meta: z.optional(z.object({}).passthrough()),
   })
@@ -57,7 +61,8 @@ export const NotificationSchema = z.object({
 export const ResultSchema = z
   .object({
     /**
-     * This result property is reserved by the protocol to allow clients and servers to attach additional metadata to their responses.
+     * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
+     * for notes on _meta usage.
      */
     _meta: z.optional(z.object({}).passthrough()),
   })
@@ -194,17 +199,34 @@ export const CancelledNotificationSchema = NotificationSchema.extend({
   }),
 });
 
-/* Initialization */
+/* Base Metadata */
 /**
- * Describes the name and version of an MCP implementation.
+ * Base metadata interface for common properties across resources, tools, prompts, and implementations.
  */
-export const ImplementationSchema = z
+export const BaseMetadataSchema = z
   .object({
+    /** Intended for programmatic or logical use, but used as a display name in past specs or fallback */
     name: z.string(),
-    version: z.string(),
+    /**
+    * Intended for UI and end-user contexts — optimized to be human-readable and easily understood,
+    * even by those unfamiliar with domain-specific terminology.
+    *
+    * If not provided, the name should be used for display (except for Tool,
+    * where `annotations.title` should be given precedence over using `name`,
+    * if present).
+    */
+    title: z.optional(z.string()),
   })
   .passthrough();
 
+/* Initialization */
+/**
+ * Describes the name and version of an MCP implementation.
+ */
+export const ImplementationSchema = BaseMetadataSchema.extend({
+  version: z.string(),
+});
+
 /**
  * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities.
  */
@@ -218,6 +240,10 @@ export const ClientCapabilitiesSchema = z
      * Present if the client supports sampling from an LLM.
      */
     sampling: z.optional(z.object({}).passthrough()),
+    /**
+     * Present if the client supports eliciting user input.
+     */
+    elicitation: z.optional(z.object({}).passthrough()),
     /**
      * Present if the client supports listing roots.
      */
@@ -417,6 +443,11 @@ export const ResourceContentsSchema = z
      * The MIME type of this resource, if known.
      */
     mimeType: z.optional(z.string()),
+    /**
+     * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
+     * for notes on _meta usage.
+     */
+    _meta: z.optional(z.object({}).passthrough()),
   })
   .passthrough();
 
@@ -437,64 +468,58 @@ export const BlobResourceContentsSchema = ResourceContentsSchema.extend({
 /**
  * A known resource that the server is capable of reading.
  */
-export const ResourceSchema = z
-  .object({
-    /**
-     * The URI of this resource.
-     */
-    uri: z.string(),
+export const ResourceSchema = BaseMetadataSchema.extend({
+  /**
+   * The URI of this resource.
+   */
+  uri: z.string(),
 
-    /**
-     * A human-readable name for this resource.
-     *
-     * This can be used by clients to populate UI elements.
-     */
-    name: z.string(),
+  /**
+   * A description of what this resource represents.
+   *
+   * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model.
+   */
+  description: z.optional(z.string()),
 
-    /**
-     * A description of what this resource represents.
-     *
-     * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model.
-     */
-    description: z.optional(z.string()),
+  /**
+   * The MIME type of this resource, if known.
+   */
+  mimeType: z.optional(z.string()),
 
-    /**
-     * The MIME type of this resource, if known.
-     */
-    mimeType: z.optional(z.string()),
-  })
-  .passthrough();
+  /**
+   * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
+   * for notes on _meta usage.
+   */
+  _meta: z.optional(z.object({}).passthrough()),
+});
 
 /**
  * A template description for resources available on the server.
  */
-export const ResourceTemplateSchema = z
-  .object({
-    /**
-     * A URI template (according to RFC 6570) that can be used to construct resource URIs.
-     */
-    uriTemplate: z.string(),
+export const ResourceTemplateSchema = BaseMetadataSchema.extend({
+  /**
+   * A URI template (according to RFC 6570) that can be used to construct resource URIs.
+   */
+  uriTemplate: z.string(),
 
-    /**
-     * A human-readable name for the type of resource this template refers to.
-     *
-     * This can be used by clients to populate UI elements.
-     */
-    name: z.string(),
+  /**
+   * A description of what this template is for.
+   *
+   * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model.
+   */
+  description: z.optional(z.string()),
 
-    /**
-     * A description of what this template is for.
-     *
-     * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model.
-     */
-    description: z.optional(z.string()),
+  /**
+   * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type.
+   */
+  mimeType: z.optional(z.string()),
 
-    /**
-     * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type.
-     */
-    mimeType: z.optional(z.string()),
-  })
-  .passthrough();
+  /**
+   * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
+   * for notes on _meta usage.
+   */
+  _meta: z.optional(z.object({}).passthrough()),
+});
 
 /**
  * Sent from the client to request a list of resources the server has.
@@ -618,22 +643,21 @@ export const PromptArgumentSchema = z
 /**
  * A prompt or prompt template that the server offers.
  */
-export const PromptSchema = z
-  .object({
-    /**
-     * The name of the prompt or prompt template.
-     */
-    name: z.string(),
-    /**
-     * An optional description of what this prompt provides
-     */
-    description: z.optional(z.string()),
-    /**
-     * A list of arguments to use for templating the prompt.
-     */
-    arguments: z.optional(z.array(PromptArgumentSchema)),
-  })
-  .passthrough();
+export const PromptSchema = BaseMetadataSchema.extend({
+  /**
+   * An optional description of what this prompt provides
+   */
+  description: z.optional(z.string()),
+  /**
+   * A list of arguments to use for templating the prompt.
+   */
+  arguments: z.optional(z.array(PromptArgumentSchema)),
+  /**
+   * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
+   * for notes on _meta usage.
+   */
+  _meta: z.optional(z.object({}).passthrough()),
+});
 
 /**
  * Sent from the client to request a list of prompts and prompt templates the server has.
@@ -676,6 +700,12 @@ export const TextContentSchema = z
      * The text content of the message.
      */
     text: z.string(),
+
+    /**
+     * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
+     * for notes on _meta usage.
+     */
+    _meta: z.optional(z.object({}).passthrough()),
   })
   .passthrough();
 
@@ -693,6 +723,12 @@ export const ImageContentSchema = z
      * The MIME type of the image. Different providers may support different image types.
      */
     mimeType: z.string(),
+
+    /**
+     * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
+     * for notes on _meta usage.
+     */
+    _meta: z.optional(z.object({}).passthrough()),
   })
   .passthrough();
 
@@ -710,6 +746,12 @@ export const AudioContentSchema = z
      * The MIME type of the audio. Different providers may support different audio types.
      */
     mimeType: z.string(),
+
+    /**
+     * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
+     * for notes on _meta usage.
+     */
+    _meta: z.optional(z.object({}).passthrough()),
   })
   .passthrough();
 
@@ -720,21 +762,41 @@ export const EmbeddedResourceSchema = z
   .object({
     type: z.literal("resource"),
     resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]),
+    /**
+     * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
+     * for notes on _meta usage.
+     */
+    _meta: z.optional(z.object({}).passthrough()),
   })
   .passthrough();
 
+/**
+ * A resource that the server is capable of reading, included in a prompt or tool call result.
+ *
+ * Note: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.
+ */
+export const ResourceLinkSchema = ResourceSchema.extend({
+  type: z.literal("resource_link"),
+});
+
+/**
+ * A content block that can be used in prompts and tool results.
+ */
+export const ContentBlockSchema = z.union([
+  TextContentSchema,
+  ImageContentSchema,
+  AudioContentSchema,
+  ResourceLinkSchema,
+  EmbeddedResourceSchema,
+]);
+
 /**
  * Describes a message returned as part of a prompt.
  */
 export const PromptMessageSchema = z
   .object({
     role: z.enum(["user", "assistant"]),
-    content: z.union([
-      TextContentSchema,
-      ImageContentSchema,
-      AudioContentSchema,
-      EmbeddedResourceSchema,
-    ]),
+    content: ContentBlockSchema,
   })
   .passthrough();
 
@@ -816,44 +878,44 @@ export const ToolAnnotationsSchema = z
 /**
  * Definition for a tool the client can call.
  */
-export const ToolSchema = z
-  .object({
-    /**
-     * The name of the tool.
-     */
-    name: z.string(),
-    /**
-     * A human-readable description of the tool.
-     */
-    description: z.optional(z.string()),
-    /**
-     * A JSON Schema object defining the expected parameters for the tool.
-     */
-    inputSchema: z
-      .object({
-        type: z.literal("object"),
-        properties: z.optional(z.object({}).passthrough()),
-        required: z.optional(z.array(z.string())),
-      })
-      .passthrough(),
-    /**
-     * An optional JSON Schema object defining the structure of the tool's output returned in 
-     * the structuredContent field of a CallToolResult.
-     */
-    outputSchema: z.optional(
-      z.object({
-        type: z.literal("object"),
-        properties: z.optional(z.object({}).passthrough()),
-        required: z.optional(z.array(z.string())),
-      })
+export const ToolSchema = BaseMetadataSchema.extend({
+  /**
+   * A human-readable description of the tool.
+   */
+  description: z.optional(z.string()),
+  /**
+   * A JSON Schema object defining the expected parameters for the tool.
+   */
+  inputSchema: z
+    .object({
+      type: z.literal("object"),
+      properties: z.optional(z.object({}).passthrough()),
+      required: z.optional(z.array(z.string())),
+    })
+    .passthrough(),
+  /**
+   * An optional JSON Schema object defining the structure of the tool's output returned in 
+   * the structuredContent field of a CallToolResult.
+   */
+  outputSchema: z.optional(
+    z.object({
+      type: z.literal("object"),
+      properties: z.optional(z.object({}).passthrough()),
+      required: z.optional(z.array(z.string())),
+    })
       .passthrough()
-    ),
-    /**
-     * Optional additional tool information.
-     */
-    annotations: z.optional(ToolAnnotationsSchema),
-  })
-  .passthrough();
+  ),
+  /**
+   * Optional additional tool information.
+   */
+  annotations: z.optional(ToolAnnotationsSchema),
+
+  /**
+   * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
+   * for notes on _meta usage.
+   */
+  _meta: z.optional(z.object({}).passthrough()),
+});
 
 /**
  * Sent from the client to request a list of tools the server has.
@@ -879,13 +941,7 @@ export const CallToolResultSchema = ResultSchema.extend({
    * If the Tool does not define an outputSchema, this field MUST be present in the result.
    * For backwards compatibility, this field is always present, but it may be empty.
    */
-  content: z.array(
-    z.union([
-      TextContentSchema,
-      ImageContentSchema,
-      AudioContentSchema,
-      EmbeddedResourceSchema,
-    ])).default([]),
+  content: z.array(ContentBlockSchema).default([]),
 
   /**
    * An object containing structured tool output.
@@ -1088,11 +1144,112 @@ export const CreateMessageResultSchema = ResultSchema.extend({
   ]),
 });
 
+/* Elicitation */
+/**
+ * Primitive schema definition for boolean fields.
+ */
+export const BooleanSchemaSchema = z
+  .object({
+    type: z.literal("boolean"),
+    title: z.optional(z.string()),
+    description: z.optional(z.string()),
+    default: z.optional(z.boolean()),
+  })
+  .passthrough();
+
+/**
+ * Primitive schema definition for string fields.
+ */
+export const StringSchemaSchema = z
+  .object({
+    type: z.literal("string"),
+    title: z.optional(z.string()),
+    description: z.optional(z.string()),
+    minLength: z.optional(z.number()),
+    maxLength: z.optional(z.number()),
+    format: z.optional(z.enum(["email", "uri", "date", "date-time"])),
+  })
+  .passthrough();
+
+/**
+ * Primitive schema definition for number fields.
+ */
+export const NumberSchemaSchema = z
+  .object({
+    type: z.enum(["number", "integer"]),
+    title: z.optional(z.string()),
+    description: z.optional(z.string()),
+    minimum: z.optional(z.number()),
+    maximum: z.optional(z.number()),
+  })
+  .passthrough();
+
+/**
+ * Primitive schema definition for enum fields.
+ */
+export const EnumSchemaSchema = z
+  .object({
+    type: z.literal("string"),
+    title: z.optional(z.string()),
+    description: z.optional(z.string()),
+    enum: z.array(z.string()),
+    enumNames: z.optional(z.array(z.string())),
+  })
+  .passthrough();
+
+/**
+ * Union of all primitive schema definitions.
+ */
+export const PrimitiveSchemaDefinitionSchema = z.union([
+  BooleanSchemaSchema,
+  StringSchemaSchema,
+  NumberSchemaSchema,
+  EnumSchemaSchema,
+]);
+
+/**
+ * A request from the server to elicit user input via the client.
+ * The client should present the message and form fields to the user.
+ */
+export const ElicitRequestSchema = RequestSchema.extend({
+  method: z.literal("elicitation/create"),
+  params: BaseRequestParamsSchema.extend({
+    /**
+     * The message to present to the user.
+     */
+    message: z.string(),
+    /**
+     * The schema for the requested user input.
+     */
+    requestedSchema: z
+      .object({
+        type: z.literal("object"),
+        properties: z.record(z.string(), PrimitiveSchemaDefinitionSchema),
+        required: z.optional(z.array(z.string())),
+      })
+      .passthrough(),
+  }),
+});
+
+/**
+ * The client's response to an elicitation/create request from the server.
+ */
+export const ElicitResultSchema = ResultSchema.extend({
+  /**
+   * The user's response action.
+   */
+  action: z.enum(["accept", "reject", "cancel"]),
+  /**
+   * The collected user input content (only present if action is "accept").
+   */
+  content: z.optional(z.record(z.string(), z.unknown())),
+});
+
 /* Autocomplete */
 /**
  * A reference to a resource or resource template definition.
  */
-export const ResourceReferenceSchema = z
+export const ResourceTemplateReferenceSchema = z
   .object({
     type: z.literal("ref/resource"),
     /**
@@ -1102,6 +1259,11 @@ export const ResourceReferenceSchema = z
   })
   .passthrough();
 
+/**
+ * @deprecated Use ResourceTemplateReferenceSchema instead
+ */
+export const ResourceReferenceSchema = ResourceTemplateReferenceSchema;
+
 /**
  * Identifies a prompt.
  */
@@ -1121,7 +1283,7 @@ export const PromptReferenceSchema = z
 export const CompleteRequestSchema = RequestSchema.extend({
   method: z.literal("completion/complete"),
   params: BaseRequestParamsSchema.extend({
-    ref: z.union([PromptReferenceSchema, ResourceReferenceSchema]),
+    ref: z.union([PromptReferenceSchema, ResourceTemplateReferenceSchema]),
     /**
      * The argument's information
      */
@@ -1137,6 +1299,14 @@ export const CompleteRequestSchema = RequestSchema.extend({
         value: z.string(),
       })
       .passthrough(),
+    context: z.optional(
+      z.object({
+        /**
+         * Previously-resolved variables in a URI template or prompt.
+         */
+        arguments: z.optional(z.record(z.string(), z.string())),
+      })
+    ),
   }),
 });
 
@@ -1176,6 +1346,12 @@ export const RootSchema = z
      * An optional name for the root.
      */
     name: z.optional(z.string()),
+
+    /**
+     * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
+     * for notes on _meta usage.
+     */
+    _meta: z.optional(z.object({}).passthrough()),
   })
   .passthrough();
 
@@ -1227,6 +1403,7 @@ export const ClientNotificationSchema = z.union([
 export const ClientResultSchema = z.union([
   EmptyResultSchema,
   CreateMessageResultSchema,
+  ElicitResultSchema,
   ListRootsResultSchema,
 ]);
 
@@ -1234,6 +1411,7 @@ export const ClientResultSchema = z.union([
 export const ServerRequestSchema = z.union([
   PingRequestSchema,
   CreateMessageRequestSchema,
+  ElicitRequestSchema,
   ListRootsRequestSchema,
 ]);
 
@@ -1286,6 +1464,36 @@ type Flatten<T> = T extends Primitive
 
 type Infer<Schema extends ZodTypeAny> = Flatten<z.infer<Schema>>;
 
+/**
+ * Headers that are compatible with both Node.js and the browser.
+ */
+export type IsomorphicHeaders = Record<string, string | string[] | undefined>;
+
+/**
+ * Information about the incoming request.
+ */
+export interface RequestInfo {
+  /**
+   * The headers of the request.
+   */
+  headers: IsomorphicHeaders;
+}
+
+/**
+ * Extra information about a message.
+ */
+export interface MessageExtraInfo {
+  /**
+   * The request information.
+   */
+  requestInfo?: RequestInfo;
+
+  /**
+   * The authentication information.
+   */
+  authInfo?: AuthInfo;
+}
+
 /* JSON-RPC types */
 export type ProgressToken = Infer<typeof ProgressTokenSchema>;
 export type Cursor = Infer<typeof CursorSchema>;
@@ -1306,6 +1514,9 @@ export type EmptyResult = Infer<typeof EmptyResultSchema>;
 /* Cancellation */
 export type CancelledNotification = Infer<typeof CancelledNotificationSchema>;
 
+/* Base Metadata */
+export type BaseMetadata = Infer<typeof BaseMetadataSchema>;
+
 /* Initialization */
 export type Implementation = Infer<typeof ImplementationSchema>;
 export type ClientCapabilities = Infer<typeof ClientCapabilitiesSchema>;
@@ -1352,6 +1563,8 @@ export type TextContent = Infer<typeof TextContentSchema>;
 export type ImageContent = Infer<typeof ImageContentSchema>;
 export type AudioContent = Infer<typeof AudioContentSchema>;
 export type EmbeddedResource = Infer<typeof EmbeddedResourceSchema>;
+export type ResourceLink = Infer<typeof ResourceLinkSchema>;
+export type ContentBlock = Infer<typeof ContentBlockSchema>;
 export type PromptMessage = Infer<typeof PromptMessageSchema>;
 export type GetPromptResult = Infer<typeof GetPromptResultSchema>;
 export type PromptListChangedNotification = Infer<typeof PromptListChangedNotificationSchema>;
@@ -1376,8 +1589,21 @@ export type SamplingMessage = Infer<typeof SamplingMessageSchema>;
 export type CreateMessageRequest = Infer<typeof CreateMessageRequestSchema>;
 export type CreateMessageResult = Infer<typeof CreateMessageResultSchema>;
 
+/* Elicitation */
+export type BooleanSchema = Infer<typeof BooleanSchemaSchema>;
+export type StringSchema = Infer<typeof StringSchemaSchema>;
+export type NumberSchema = Infer<typeof NumberSchemaSchema>;
+export type EnumSchema = Infer<typeof EnumSchemaSchema>;
+export type PrimitiveSchemaDefinition = Infer<typeof PrimitiveSchemaDefinitionSchema>;
+export type ElicitRequest = Infer<typeof ElicitRequestSchema>;
+export type ElicitResult = Infer<typeof ElicitResultSchema>;
+
 /* Autocomplete */
-export type ResourceReference = Infer<typeof ResourceReferenceSchema>;
+export type ResourceTemplateReference = Infer<typeof ResourceTemplateReferenceSchema>;
+/**
+ * @deprecated Use ResourceTemplateReference instead
+ */
+export type ResourceReference = ResourceTemplateReference;
 export type PromptReference = Infer<typeof PromptReferenceSchema>;
 export type CompleteRequest = Infer<typeof CompleteRequestSchema>;
 export type CompleteResult = Infer<typeof CompleteResultSchema>;



Test patch:
diff --git a/src/integration-tests/stateManagementStreamableHttp.test.ts b/src/integration-tests/stateManagementStreamableHttp.test.ts
index b7ff17e..4a19113 100644
--- a/src/integration-tests/stateManagementStreamableHttp.test.ts
+++ b/src/integration-tests/stateManagementStreamableHttp.test.ts
@@ -5,7 +5,7 @@ import { Client } from '../client/index.js';
 import { StreamableHTTPClientTransport } from '../client/streamableHttp.js';
 import { McpServer } from '../server/mcp.js';
 import { StreamableHTTPServerTransport } from '../server/streamableHttp.js';
-import { CallToolResultSchema, ListToolsResultSchema, ListResourcesResultSchema, ListPromptsResultSchema } from '../types.js';
+import { CallToolResultSchema, ListToolsResultSchema, ListResourcesResultSchema, ListPromptsResultSchema, LATEST_PROTOCOL_VERSION } from '../types.js';
 import { z } from 'zod';
 
 describe('Streamable HTTP Transport Session Management', () => {
@@ -145,7 +145,7 @@ describe('Streamable HTTP Transport Session Management', () => {
         params: {}
       }, ListToolsResultSchema);
 
-      
+
     });
     it('should operate without session management', async () => {
       // Create and connect a client
@@ -211,6 +211,27 @@ describe('Streamable HTTP Transport Session Management', () => {
       // Clean up
       await transport.close();
     });
+
+    it('should set protocol version after connecting', async () => {
+      // Create and connect a client
+      const client = new Client({
+        name: 'test-client',
+        version: '1.0.0'
+      });
+
+      const transport = new StreamableHTTPClientTransport(baseUrl);
+
+      // Verify protocol version is not set before connecting
+      expect(transport.protocolVersion).toBeUndefined();
+
+      await client.connect(transport);
+
+      // Verify protocol version is set after connecting
+      expect(transport.protocolVersion).toBe(LATEST_PROTOCOL_VERSION);
+
+      // Clean up
+      await transport.close();
+    });
   });
 
   describe('Stateful Mode', () => {