Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions packages/mcp-server/src/features/fetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ export function registerFetchTool(tools: ToolRegistry, server: Server) {
type({
name: '"fetch"',
arguments: {
url: "string",
"maxLength?": type("number").describe("Limit response length."),
url: type("string").describe("The URL to fetch content from. REQUIRED."),
"maxLength?": type("number").describe(
"Limit response length. Optional.",
),
"startIndex?": type("number").describe(
"Supports paginated retrieval of content.",
"Supports paginated retrieval of content. Optional.",
),
"raw?": type("boolean").describe(
"Returns raw HTML content if raw=true.",
"Returns raw HTML content if raw=true. Optional.",
),
},
}).describe(
"Reads and returns the content of any web page. Returns the content in Markdown format by default, or can return raw HTML if raw=true parameter is set. Supports pagination through maxLength and startIndex parameters.",
"Reads and returns the content of any web page. Returns the content in Markdown format by default, or can return raw HTML if raw=true parameter is set. Supports pagination through maxLength and startIndex parameters. " +
"IMPORTANT: The 'url' argument is required. " +
"Example: { url: 'https://example.com/page', maxLength: 5000 }",
),
async ({ arguments: args }) => {
logger.info("Fetching URL", { url: args.url });
Expand Down
136 changes: 105 additions & 31 deletions packages/mcp-server/src/features/local-rest-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
type({
name: '"get_active_file"',
arguments: {
format: type('"markdown" | "json"').optional(),
"format?": type('"markdown" | "json"').describe(
"Response format: 'markdown' for raw content or 'json' for parsed with frontmatter and tags. Optional, defaults to 'markdown'.",
),
},
}).describe(
"Returns the content of the currently active file in Obsidian. Can return either markdown content or a JSON representation including parsed tags and frontmatter.",
"Returns the content of the currently active file in Obsidian. Can return either markdown content or a JSON representation including parsed tags and frontmatter. " +
"Example: { format: 'json' }",
),
async ({ arguments: args }) => {
const format =
Expand All @@ -53,9 +56,15 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
type({
name: '"update_active_file"',
arguments: {
content: "string",
content: type("string").describe(
"The complete new content to write to the file. REQUIRED.",
),
},
}).describe("Update the content of the active file open in Obsidian."),
}).describe(
"Update the content of the active file open in Obsidian. " +
"IMPORTANT: The 'content' argument is required. " +
"Example: { content: '# My Note\\n\\nUpdated content here' }",
),
async ({ arguments: args }) => {
await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", {
method: "PUT",
Expand All @@ -72,9 +81,15 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
type({
name: '"append_to_active_file"',
arguments: {
content: "string",
content: type("string").describe(
"The content to append to the end of the file. REQUIRED.",
),
},
}).describe("Append content to the end of the currently-open note."),
}).describe(
"Append content to the end of the currently-open note. " +
"IMPORTANT: The 'content' argument is required. " +
"Example: { content: '\\n\\n## New Section\\n\\nAppended content' }",
),
async ({ arguments: args }) => {
await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", {
method: "POST",
Expand All @@ -92,7 +107,9 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
name: '"patch_active_file"',
arguments: LocalRestAPI.ApiPatchParameters,
}).describe(
"Insert or modify content in the currently-open note relative to a heading, block reference, or frontmatter field.",
"Insert or modify content in the currently-open note relative to a heading, block reference, or frontmatter field. " +
"IMPORTANT: The 'operation', 'targetType', 'target', and 'content' arguments are required. " +
"Example: { operation: 'append', targetType: 'heading', target: 'My Heading', content: '\\n\\nNew content' }",
),
async ({ arguments: args }) => {
const headers: Record<string, string> = {
Expand Down Expand Up @@ -151,11 +168,17 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
type({
name: '"show_file_in_obsidian"',
arguments: {
filename: "string",
"newLeaf?": "boolean",
filename: type("string").describe(
"The vault-relative path to the file (e.g., 'notes/my-note.md'). REQUIRED.",
),
"newLeaf?": type("boolean").describe(
"Whether to open in a new tab. Optional, defaults to false.",
),
},
}).describe(
"Open a document in the Obsidian UI. Creates a new document if it doesn't exist. Returns a confirmation if the file was opened successfully.",
"Open a document in the Obsidian UI. Creates a new document if it doesn't exist. " +
"IMPORTANT: The 'filename' argument is required. " +
"Example: { filename: 'daily/2025-01-17.md' }",
),
async ({ arguments: args }) => {
const query = args.newLeaf ? "?newLeaf=true" : "";
Expand All @@ -179,11 +202,17 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
type({
name: '"search_vault"',
arguments: {
queryType: '"dataview" | "jsonlogic"',
query: "string",
queryType: type('"dataview" | "jsonlogic"').describe(
"The query language to use: 'dataview' for DQL or 'jsonlogic'. REQUIRED.",
),
query: type("string").describe(
"The search query string in the specified query language. REQUIRED.",
),
},
}).describe(
"Search for documents matching a specified query using either Dataview DQL or JsonLogic.",
"Search for documents matching a specified query using either Dataview DQL or JsonLogic. " +
"IMPORTANT: Both 'queryType' and 'query' arguments are required. " +
"Example: { queryType: 'dataview', query: 'LIST FROM \"notes\"' }",
),
async ({ arguments: args }) => {
const contentType =
Expand Down Expand Up @@ -212,10 +241,18 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
type({
name: '"search_vault_simple"',
arguments: {
query: "string",
"contextLength?": "number",
query: type("string").describe(
"The text search query to find in vault files. REQUIRED.",
),
"contextLength?": type("number").describe(
"Number of characters of context around matches. Optional.",
),
},
}).describe("Search for documents matching a text query."),
}).describe(
"Search for documents matching a text query. " +
"IMPORTANT: The 'query' argument is required. " +
"Example: { query: 'meeting notes' }",
),
async ({ arguments: args }) => {
const query = new URLSearchParams({
query: args.query,
Expand Down Expand Up @@ -245,10 +282,13 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
type({
name: '"list_vault_files"',
arguments: {
"directory?": "string",
"directory?": type("string").describe(
"The vault-relative path to list. Optional, defaults to vault root.",
),
},
}).describe(
"List files in the root directory or a specified subdirectory of your vault.",
"List files in the root directory or a specified subdirectory of your vault. " +
"Example: { directory: 'notes/projects' }",
),
async ({ arguments: args }) => {
const path = args.directory ? `${args.directory}/` : "";
Expand All @@ -269,10 +309,18 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
type({
name: '"get_vault_file"',
arguments: {
filename: "string",
"format?": '"markdown" | "json"',
filename: type("string").describe(
"The vault-relative path to the file (e.g., 'notes/my-note.md'). REQUIRED.",
),
"format?": type('"markdown" | "json"').describe(
"Response format: 'markdown' for raw content or 'json' for parsed with frontmatter. Optional, defaults to 'markdown'.",
),
},
}).describe("Get the content of a file from your vault."),
}).describe(
"Get the content of a file from your vault. " +
"IMPORTANT: The 'filename' argument is required. " +
"Example: { filename: 'daily/2025-01-17.md' }",
),
async ({ arguments: args }) => {
const isJson = args.format === "json";
const format = isJson
Expand Down Expand Up @@ -302,10 +350,18 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
type({
name: '"create_vault_file"',
arguments: {
filename: "string",
content: "string",
filename: type("string").describe(
"The vault-relative path to the file (e.g., 'notes/my-note.md'). REQUIRED.",
),
content: type("string").describe(
"The complete content to write to the file. REQUIRED.",
),
},
}).describe("Create a new file in your vault or update an existing one."),
}).describe(
"Create a new file in your vault or update an existing file. " +
"IMPORTANT: Both 'filename' and 'content' arguments are required. " +
"Example: { filename: 'daily/2025-01-17.md', content: '# Daily Note\\n\\nContent here' }",
),
async ({ arguments: args }) => {
await makeRequest(
LocalRestAPI.ApiNoContentResponse,
Expand All @@ -326,10 +382,18 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
type({
name: '"append_to_vault_file"',
arguments: {
filename: "string",
content: "string",
filename: type("string").describe(
"The vault-relative path to the file (e.g., 'notes/my-note.md'). REQUIRED.",
),
content: type("string").describe(
"The content to append to the end of the file. REQUIRED.",
),
},
}).describe("Append content to a new or existing file."),
}).describe(
"Append content to a new or existing file. " +
"IMPORTANT: Both 'filename' and 'content' arguments are required. " +
"Example: { filename: 'journal/log.md', content: '\\n\\n## New Entry\\n\\nContent here' }",
),
async ({ arguments: args }) => {
await makeRequest(
LocalRestAPI.ApiNoContentResponse,
Expand All @@ -350,10 +414,14 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
type({
name: '"patch_vault_file"',
arguments: type({
filename: "string",
filename: type("string").describe(
"The vault-relative path to the file (e.g., 'notes/my-note.md'). REQUIRED.",
),
}).and(LocalRestAPI.ApiPatchParameters),
}).describe(
"Insert or modify content in a file relative to a heading, block reference, or frontmatter field.",
"Insert or modify content in a file relative to a heading, block reference, or frontmatter field. " +
"IMPORTANT: The 'filename', 'operation', 'targetType', 'target', and 'content' arguments are required. " +
"Example: { filename: 'notes/todo.md', operation: 'append', targetType: 'heading', target: 'Tasks', content: '\\n- [ ] New task' }",
),
async ({ arguments: args }) => {
const headers: HeadersInit = {
Expand Down Expand Up @@ -397,9 +465,15 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) {
type({
name: '"delete_vault_file"',
arguments: {
filename: "string",
filename: type("string").describe(
"The vault-relative path to the file to delete (e.g., 'notes/old-note.md'). REQUIRED.",
),
},
}).describe("Delete a file from your vault."),
}).describe(
"Delete a file from your vault. " +
"IMPORTANT: The 'filename' argument is required. " +
"Example: { filename: 'archive/old-note.md' }",
),
async ({ arguments: args }) => {
await makeRequest(
LocalRestAPI.ApiNoContentResponse,
Expand Down
16 changes: 11 additions & 5 deletions packages/mcp-server/src/features/smart-connections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,26 @@ export function registerSmartConnectionsTools(tools: ToolRegistry) {
type({
name: '"search_vault_smart"',
arguments: {
query: type("string>0").describe("A search phrase for semantic search"),
query: type("string>0").describe(
"A search phrase for semantic search. REQUIRED.",
),
"filter?": {
"folders?": type("string[]").describe(
'An array of folder names to include. For example, ["Public", "Work"]',
"An array of folder names to include. For example, ['Public', 'Work']. Optional.",
),
"excludeFolders?": type("string[]").describe(
'An array of folder names to exclude. For example, ["Private", "Archive"]',
"An array of folder names to exclude. For example, ['Private', 'Archive']. Optional.",
),
"limit?": type("number>0").describe(
"The maximum number of results to return",
"The maximum number of results to return. Optional.",
),
},
},
}).describe("Search for documents semantically matching a text string."),
}).describe(
"Search for documents semantically matching a text string. " +
"IMPORTANT: The 'query' argument is required. " +
"Example: { query: 'meeting notes about project planning', filter: { folders: ['Work'], limit: 10 } }",
),
async ({ arguments: args }) => {
const data = await makeRequest(
LocalRestAPI.ApiSmartSearchResponse,
Expand Down
10 changes: 8 additions & 2 deletions packages/mcp-server/src/features/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ export function registerTemplaterTools(tools: ToolRegistry) {
arguments: LocalRestAPI.ApiTemplateExecutionParams.omit("createFile").and(
{
// should be boolean but the MCP client returns a string
"createFile?": type("'true'|'false'"),
"createFile?": type("'true'|'false'").describe(
"Whether to create a new file from the template. Optional.",
),
},
),
}).describe("Execute a Templater template with the given arguments"),
}).describe(
"Execute a Templater template with the given arguments. " +
"IMPORTANT: The 'name' argument is required. " +
"Example: { name: 'templates/daily-note.md', arguments: { title: 'My Title' }, createFile: 'true', targetPath: 'notes/output.md' }",
),
async ({ arguments: args }) => {
// Get prompt content
const data = await makeRequest(
Expand Down
10 changes: 10 additions & 0 deletions packages/mcp-server/src/shared/ToolRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ export class ToolRegistryClass<
context: HandlerContext,
) => {
try {
const argCount = params.arguments
? Object.keys(params.arguments).length
: 0;
if (argCount === 0) {
logger.warn(`Tool "${params.name}" called with no arguments`, {
name: params.name,
arguments: params.arguments,
});
}

for (const [schema, handler] of this.entries()) {
if (schema.get("name").allows(params.name)) {
const validParams = schema.assert(
Expand Down
5 changes: 4 additions & 1 deletion packages/mcp-server/src/shared/formatMcpError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export function formatMcpError(error: unknown) {

if (error instanceof type.errors) {
const message = error.summary;
return new McpError(ErrorCode.InvalidParams, message);
const hint = message.includes("must be")
? " HINT: Ensure all required parameters are included in the arguments object."
: "";
return new McpError(ErrorCode.InvalidParams, message + hint);
}

if (type({ message: "string" }).allows(error)) {
Expand Down
12 changes: 8 additions & 4 deletions packages/shared/src/types/plugin-local-rest-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,17 @@ export const ApiNoContentResponse = type("unknown").describe("No Content");
* @property targetPath - The path to save the file; required if createFile is true
*/
export const ApiTemplateExecutionParams = type({
name: type("string").describe("The full vault path to the template file"),
arguments: "Record<string, string>",
name: type("string").describe(
"The full vault path to the template file. REQUIRED.",
),
arguments: type("Record<string, string>").describe(
"Key-value pairs of template parameters. The keys should match tp.mcpTools.prompt() calls in the template.",
),
"createFile?": type("boolean").describe(
"Whether to create a new file from the template",
"Whether to create a new file from the template. Optional.",
),
"targetPath?": type("string").describe(
"Path to save the file; required if createFile is true",
"Path to save the file; required if createFile is true. Optional.",
),
});

Expand Down