diff --git a/scripts/release.py b/scripts/release.py index e4ce1274c3..7d0e37c100 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -113,7 +113,12 @@ def has_changes(path: Path, git_hash: GitHash) -> bool: ) changed_files = [Path(f) for f in output.stdout.splitlines()] - relevant_files = [f for f in changed_files if f.suffix in [".py", ".ts"]] + # Include source files and lock files to trigger version bumps + relevant_files = [ + f + for f in changed_files + if f.suffix in [".py", ".ts"] or f.name in ["uv.lock", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"] + ] return len(relevant_files) >= 1 except subprocess.CalledProcessError: return False diff --git a/src/everything/__tests__/tools.test.ts b/src/everything/__tests__/tools.test.ts index dbe463b2a5..01637472d8 100644 --- a/src/everything/__tests__/tools.test.ts +++ b/src/everything/__tests__/tools.test.ts @@ -151,31 +151,34 @@ describe('Tools', () => { }); describe('get-env', () => { - it('should return all environment variables as JSON', async () => { + it('should return a specific environment variable', async () => { const { mockServer, handlers } = createMockServer(); registerGetEnvTool(mockServer); const handler = handlers.get('get-env')!; process.env.TEST_VAR_EVERYTHING = 'test_value'; - const result = await handler({}); + const result = await handler({ key: 'TEST_VAR_EVERYTHING' }); expect(result.content).toHaveLength(1); expect(result.content[0].type).toBe('text'); - - const envJson = JSON.parse(result.content[0].text); - expect(envJson.TEST_VAR_EVERYTHING).toBe('test_value'); + expect(result.content[0].text).toBe('TEST_VAR_EVERYTHING=test_value'); delete process.env.TEST_VAR_EVERYTHING; }); - it('should return valid JSON', async () => { + it('should return error for undefined variable', async () => { const { mockServer, handlers } = createMockServer(); registerGetEnvTool(mockServer); const handler = handlers.get('get-env')!; - const result = await handler({}); + const result = await handler({ key: 'NON_EXISTENT_VAR_12345' }); - expect(() => JSON.parse(result.content[0].text)).not.toThrow(); + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toBe('text'); + expect(result.content[0].text).toBe( + "Environment variable 'NON_EXISTENT_VAR_12345' is not set.", + ); + expect(result.isError).toBe(true); }); }); diff --git a/src/everything/tools/get-env.ts b/src/everything/tools/get-env.ts index 0adbf5a14d..3c078484d6 100644 --- a/src/everything/tools/get-env.ts +++ b/src/everything/tools/get-env.ts @@ -4,28 +4,53 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; // Tool configuration const name = "get-env"; const config = { - title: "Print Environment Tool", + title: "Get Environment Variable", description: - "Returns all environment variables, helpful for debugging MCP server configuration", - inputSchema: {}, + "Returns a specific environment variable value by name. Use this to check individual configuration values.", + inputSchema: { + type: "object", + properties: { + key: { + type: "string", + description: "Name of the environment variable to retrieve", + }, + }, + required: ["key"], + }, }; /** * Registers the 'get-env' tool. * - * The registered tool Retrieves and returns the environment variables - * of the current process as a JSON-formatted string encapsulated in a text response. + * The registered tool retrieves and returns a specific environment variable + * by name. This prevents accidental exposure of sensitive environment variables + * that may be present in the full process.env object. * * @param {McpServer} server - The McpServer instance where the tool will be registered. * @returns {void} */ export const registerGetEnvTool = (server: McpServer) => { server.registerTool(name, config, async (args): Promise => { + const key = args.key as string; + const value = process.env[key]; + + if (value === undefined) { + return { + content: [ + { + type: "text", + text: `Environment variable '${key}' is not set.`, + }, + ], + isError: true, + }; + } + return { content: [ { type: "text", - text: JSON.stringify(process.env, null, 2), + text: `${key}=${value}`, }, ], }; diff --git a/src/everything/tools/get-resource-reference.ts b/src/everything/tools/get-resource-reference.ts index d3dc5d3ecb..b86c23ca38 100644 --- a/src/everything/tools/get-resource-reference.ts +++ b/src/everything/tools/get-resource-reference.ts @@ -15,7 +15,10 @@ import { const GetResourceReferenceSchema = z.object({ resourceType: z .enum([RESOURCE_TYPE_TEXT, RESOURCE_TYPE_BLOB]) - .default(RESOURCE_TYPE_TEXT), + .default(RESOURCE_TYPE_TEXT) + .describe( + `Type of resource — must be '${RESOURCE_TYPE_TEXT}' or '${RESOURCE_TYPE_BLOB}'.`, + ), resourceId: z .number() .default(1) diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 7b67e63e58..70c2900bb8 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -94,13 +94,13 @@ setAllowedDirectories(allowedDirectories); // Schema definitions const ReadTextFileArgsSchema = z.object({ - path: z.string(), + path: z.string().describe("Absolute path to the file to read"), tail: z.number().optional().describe('If provided, returns only the last N lines of the file'), head: z.number().optional().describe('If provided, returns only the first N lines of the file') }); const ReadMediaFileArgsSchema = z.object({ - path: z.string() + path: z.string().describe("Absolute path to the media file to read") }); const ReadMultipleFilesArgsSchema = z.object({ @@ -111,8 +111,8 @@ const ReadMultipleFilesArgsSchema = z.object({ }); const WriteFileArgsSchema = z.object({ - path: z.string(), - content: z.string(), + path: z.string().describe("Absolute path to the file to write"), + content: z.string().describe("Content to write to the file"), }); const EditOperation = z.object({ @@ -121,42 +121,42 @@ const EditOperation = z.object({ }); const EditFileArgsSchema = z.object({ - path: z.string(), - edits: z.array(EditOperation), + path: z.string().describe("Absolute path to the file to edit"), + edits: z.array(EditOperation).describe("Array of edit operations to apply"), dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format') }); const CreateDirectoryArgsSchema = z.object({ - path: z.string(), + path: z.string().describe("Absolute path to the directory to create"), }); const ListDirectoryArgsSchema = z.object({ - path: z.string(), + path: z.string().describe("Absolute path to the directory to list"), }); const ListDirectoryWithSizesArgsSchema = z.object({ - path: z.string(), + path: z.string().describe("Absolute path to the directory to list"), sortBy: z.enum(['name', 'size']).optional().default('name').describe('Sort entries by name or size'), }); const DirectoryTreeArgsSchema = z.object({ - path: z.string(), - excludePatterns: z.array(z.string()).optional().default([]) + path: z.string().describe("Absolute path to the directory to get tree for"), + excludePatterns: z.array(z.string()).optional().default([]).describe("Glob patterns to exclude from the tree") }); const MoveFileArgsSchema = z.object({ - source: z.string(), - destination: z.string(), + source: z.string().describe("Absolute path of the file or directory to move"), + destination: z.string().describe("Absolute path of the destination location"), }); const SearchFilesArgsSchema = z.object({ - path: z.string(), - pattern: z.string(), - excludePatterns: z.array(z.string()).optional().default([]) + path: z.string().describe("Absolute path to the directory to search in"), + pattern: z.string().describe("Glob pattern to match files against"), + excludePatterns: z.array(z.string()).optional().default([]).describe("Glob patterns to exclude from search") }); const GetFileInfoArgsSchema = z.object({ - path: z.string(), + path: z.string().describe("Absolute path to the file or directory to get info for"), }); // Server setup @@ -235,7 +235,7 @@ server.registerTool( "the last N lines of a file. Operates on the file as text regardless of extension. " + "Only works within allowed directories.", inputSchema: { - path: z.string(), + path: z.string().describe("Absolute path to the file to read"), tail: z.number().optional().describe("If provided, returns only the last N lines of the file"), head: z.number().optional().describe("If provided, returns only the first N lines of the file") }, @@ -253,7 +253,7 @@ server.registerTool( "Read an image or audio file. Returns the base64 encoded data and MIME type. " + "Only works within allowed directories.", inputSchema: { - path: z.string() + path: z.string().describe("Absolute path to the media file to read") }, outputSchema: { content: z.array(z.object({ @@ -345,8 +345,8 @@ server.registerTool( "Use with caution as it will overwrite existing files without warning. " + "Handles text content with proper encoding. Only works within allowed directories.", inputSchema: { - path: z.string(), - content: z.string() + path: z.string().describe("Absolute path to the file to write"), + content: z.string().describe("Content to write to the file") }, outputSchema: { content: z.string() }, annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true } @@ -371,11 +371,11 @@ server.registerTool( "with new content. Returns a git-style diff showing the changes made. " + "Only works within allowed directories.", inputSchema: { - path: z.string(), + path: z.string().describe("Absolute path to the file to edit"), edits: z.array(z.object({ oldText: z.string().describe("Text to search for - must match exactly"), newText: z.string().describe("Text to replace with") - })), + })).describe("Array of edit operations to apply"), dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format") }, outputSchema: { content: z.string() }, @@ -401,7 +401,7 @@ server.registerTool( "this operation will succeed silently. Perfect for setting up directory " + "structures for projects or ensuring required paths exist. Only works within allowed directories.", inputSchema: { - path: z.string() + path: z.string().describe("Absolute path to the directory to create") }, outputSchema: { content: z.string() }, annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false } @@ -427,7 +427,7 @@ server.registerTool( "prefixes. This tool is essential for understanding directory structure and " + "finding specific files within a directory. Only works within allowed directories.", inputSchema: { - path: z.string() + path: z.string().describe("Absolute path to the directory to list") }, outputSchema: { content: z.string() }, annotations: { readOnlyHint: true } @@ -455,7 +455,7 @@ server.registerTool( "prefixes. This tool is useful for understanding directory structure and " + "finding specific files within a directory. Only works within allowed directories.", inputSchema: { - path: z.string(), + path: z.string().describe("Absolute path to the directory to list"), sortBy: z.enum(["name", "size"]).optional().default("name").describe("Sort entries by name or size") }, outputSchema: { content: z.string() }, @@ -534,8 +534,8 @@ server.registerTool( "Files have no children array, while directories always have a children array (which may be empty). " + "The output is formatted with 2-space indentation for readability. Only works within allowed directories.", inputSchema: { - path: z.string(), - excludePatterns: z.array(z.string()).optional().default([]) + path: z.string().describe("Absolute path to the directory to get tree for"), + excludePatterns: z.array(z.string()).optional().default([]).describe("Glob patterns to exclude from the tree") }, outputSchema: { content: z.string() }, annotations: { readOnlyHint: true } @@ -604,8 +604,8 @@ server.registerTool( "operation will fail. Works across different directories and can be used " + "for simple renaming within the same directory. Both source and destination must be within allowed directories.", inputSchema: { - source: z.string(), - destination: z.string() + source: z.string().describe("Absolute path of the file or directory to move"), + destination: z.string().describe("Absolute path of the destination location") }, outputSchema: { content: z.string() }, annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true } @@ -634,9 +634,9 @@ server.registerTool( "Returns full paths to all matching items. Great for finding files when you don't know their exact location. " + "Only searches within allowed directories.", inputSchema: { - path: z.string(), - pattern: z.string(), - excludePatterns: z.array(z.string()).optional().default([]) + path: z.string().describe("Absolute path to the directory to search in"), + pattern: z.string().describe("Glob pattern to match files against"), + excludePatterns: z.array(z.string()).optional().default([]).describe("Glob patterns to exclude from search") }, outputSchema: { content: z.string() }, annotations: { readOnlyHint: true } @@ -662,7 +662,7 @@ server.registerTool( "and type. This tool is perfect for understanding file characteristics " + "without reading the actual content. Only works within allowed directories.", inputSchema: { - path: z.string() + path: z.string().describe("Absolute path to the file or directory to get info for") }, outputSchema: { content: z.string() }, annotations: { readOnlyHint: true }