mirror of
https://github.com/cline/cline.git
synced 2026-09-14 11:29:25 +08:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4443f3e933 | ||
|
|
cc4035141a | ||
|
|
8cc64f5e7e | ||
|
|
a4412e8014 | ||
|
|
36f7abb8ec | ||
|
|
080a79bd7d | ||
|
|
0208fdf555 | ||
|
|
226f20f28f | ||
|
|
fdc76c8802 | ||
|
|
7099a00674 | ||
|
|
b470229a97 | ||
|
|
e37f6e3b88 | ||
|
|
4c72bd96ab | ||
|
|
be120e85be | ||
|
|
fdd04bc942 | ||
|
|
b7c03af9ac | ||
|
|
1961583eb6 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
context management for file reads
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix auto approve state out of sync
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
TS errors
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Reduce system prompt size by letting Cline dynamically load MCP documentation only when needed
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Feature to open basic settings & scroll a section into view with a highlight animation
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Disable notifications in browser
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add modal for toggling MCP servers to the chat area
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Features to Relaunch browser in debug, test connection
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added option to auto-approve all commands
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remote browser control via devtools protocol
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
factor out servers list
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix tool toggle auto approve
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
allow enabling prompt caching for LiteLLM + Claude
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added feature to detect installed versions of chromium and display them as a placeholder if not already explicitly configured by the user
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix for headless browser mode
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Drag and drop of file/folders into cline chat
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
formatted read files tool
|
||||
@@ -13,7 +13,10 @@
|
||||
- [ ] 🐛 Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] ✨ New feature (non-breaking change which adds functionality)
|
||||
- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] ♻️ Refactor Changes
|
||||
- [ ] 💅 Cosmetic Changes
|
||||
- [ ] 📚 Documentation update
|
||||
- [ ] 🏃 Workflow Changes
|
||||
|
||||
### Pre-flight Checklist
|
||||
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## [3.10.1]
|
||||
|
||||
- Add CMD+' keyboard shortcut to add selected text to Cline
|
||||
- Cline now auto focuses the text field when using 'Add to Cline' shortcut
|
||||
- Add new 'Create New Task' tool to let Cline start a new task autonomously!
|
||||
- Fix Mermaid diagram issues
|
||||
- Fix Gemini provider cost calculation to take new tiered pricing structure into account
|
||||
|
||||
## [3.10.0]
|
||||
|
||||
- Add setting to let browser tool use local Chrome via remote debugging, enabling session-based browsing. Replaces sessionless Chromium, unlocking debugging and productivity workflows tied to your real browser state.
|
||||
- Add new auto-approve option to approve _ALL_ commands (use at your own risk!)
|
||||
- Add modal in the chat area to more easily enable or disable MCP servers
|
||||
- Add drag and drop of file/folders into cline chat (Thanks eljapi!)
|
||||
- Add prompt caching for LiteLLM + Claude (Thanks sammcj!)
|
||||
- Add Improved context management
|
||||
- Fix MCP auto approve toggle issues being out of sync with settings
|
||||
|
||||
## [3.9.2]
|
||||
|
||||
- Add recommended models for Cline provider
|
||||
|
||||
@@ -82,6 +82,7 @@ Cline has access to the following tools for various tasks:
|
||||
4. **Interaction Tools**
|
||||
- `ask_followup_question`: Ask user for clarification
|
||||
- `attempt_completion`: Present final results
|
||||
- `new_task`: Start a new task with preloaded context
|
||||
|
||||
Each tool has specific parameters and usage patterns. Here are some examples:
|
||||
|
||||
@@ -114,6 +115,21 @@ Each tool has specific parameters and usage patterns. Here are some examples:
|
||||
</execute_command>
|
||||
```
|
||||
|
||||
- Start a new task with context (new_task):
|
||||
```xml
|
||||
<new_task>
|
||||
<context>
|
||||
We've completed the backend API with these endpoints:
|
||||
- GET /api/tasks
|
||||
- POST /api/tasks
|
||||
- PUT /api/tasks/:id
|
||||
- DELETE /api/tasks/:id
|
||||
|
||||
Now we need to implement the React frontend.
|
||||
</context>
|
||||
</new_task>
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
1. **Create a New Component**
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.9.2",
|
||||
"version": "3.10.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.9.2",
|
||||
"version": "3.10.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
|
||||
+28
-2
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.9.2",
|
||||
"version": "3.10.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -118,6 +118,16 @@
|
||||
"category": "Cline"
|
||||
}
|
||||
],
|
||||
"keybindings": [
|
||||
{
|
||||
"command": "cline.addToChat",
|
||||
"key": "cmd+'",
|
||||
"mac": "cmd+'",
|
||||
"win": "ctrl+'",
|
||||
"linux": "ctrl+'",
|
||||
"when": "editorHasSelection"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"view/title": [
|
||||
{
|
||||
@@ -214,6 +224,21 @@
|
||||
},
|
||||
"description": "Settings for VSCode Language Model API"
|
||||
},
|
||||
"cline.mcp.mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"full",
|
||||
"server-use-only",
|
||||
"off"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Enable all MCP functionality (server use and build instructions)",
|
||||
"Enable MCP server use only (excludes instructions about building MCP servers)",
|
||||
"Disable all MCP functionality"
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Controls MCP inclusion in prompts, reduces token usage if you only need access to certain functionality."
|
||||
},
|
||||
"cline.enableCheckpoints": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -286,8 +311,9 @@
|
||||
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts",
|
||||
"format": "prettier . --check",
|
||||
"format:fix": "prettier . --write",
|
||||
"test": "vscode-test",
|
||||
"test": "npm-run-all test:unit test:integration",
|
||||
"test:ci": "node scripts/test-ci.js",
|
||||
"test:integration": "vscode-test",
|
||||
"test:unit": "TS_NODE_PROJECT='./tsconfig.unit-test.json' mocha",
|
||||
"test:coverage": "vscode-test --coverage",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
|
||||
+2
-2
@@ -9,10 +9,10 @@ try {
|
||||
execSync("which xvfb-run", { stdio: "ignore" })
|
||||
|
||||
console.log("xvfb-run is installed. Running tests with xvfb-run...")
|
||||
execSync("xvfb-run -a npm run test", { stdio: "inherit" })
|
||||
execSync("xvfb-run -a npm run test:integration", { stdio: "inherit" })
|
||||
} else {
|
||||
console.log("Non-Linux environment detected. Running tests normally.")
|
||||
execSync("npm run test", { stdio: "inherit" })
|
||||
execSync("npm run test:integration", { stdio: "inherit" })
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.platform === "linux") {
|
||||
|
||||
@@ -21,8 +21,8 @@ export const toolUseNames = [
|
||||
"access_mcp_resource",
|
||||
"ask_followup_question",
|
||||
"plan_mode_respond",
|
||||
"load_mcp_documentation",
|
||||
"attempt_completion",
|
||||
"new_task",
|
||||
] as const
|
||||
|
||||
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)
|
||||
@@ -49,6 +49,7 @@ export const toolParamNames = [
|
||||
"options",
|
||||
"response",
|
||||
"result",
|
||||
"context",
|
||||
] as const
|
||||
|
||||
export type ToolParamName = (typeof toolParamNames)[number]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ClineApiReqInfo, ClineMessage } from "../../shared/ExtensionMessage"
|
||||
import { ApiHandler } from "../../api"
|
||||
import { OpenAiHandler } from "../../api/providers/openai"
|
||||
import { getContextWindowInfo } from "./context-window-utils"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { GlobalFileNames } from "../storage/disk"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
@@ -125,25 +125,7 @@ export class ContextManager {
|
||||
const timestamp = previousRequest.ts
|
||||
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
|
||||
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
|
||||
let contextWindow = api.getModel().info.contextWindow || 128_000
|
||||
// FIXME: hack to get anyone using openai compatible with deepseek to have the proper context window instead of the default 128k. We need a way for the user to specify the context window for models they input through openai compatible
|
||||
if (api instanceof OpenAiHandler && api.getModel().id.toLowerCase().includes("deepseek")) {
|
||||
contextWindow = 64_000
|
||||
}
|
||||
let maxAllowedSize: number
|
||||
switch (contextWindow) {
|
||||
case 64_000: // deepseek models
|
||||
maxAllowedSize = contextWindow - 27_000
|
||||
break
|
||||
case 128_000: // most models
|
||||
maxAllowedSize = contextWindow - 30_000
|
||||
break
|
||||
case 200_000: // claude models
|
||||
maxAllowedSize = contextWindow - 40_000
|
||||
break
|
||||
default:
|
||||
maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8) // for deepseek, 80% of 64k meant only ~10k buffer which was too small and resulted in users getting context window errors.
|
||||
}
|
||||
const { maxAllowedSize } = getContextWindowInfo(api)
|
||||
|
||||
// This is the most reliable way to know when we're close to hitting the context window.
|
||||
if (totalTokens >= maxAllowedSize) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ApiHandler } from "../../api"
|
||||
import { OpenAiHandler } from "../../api/providers/openai"
|
||||
|
||||
/**
|
||||
* Gets context window information for the given API handler
|
||||
*
|
||||
* @param api The API handler to get context window information for
|
||||
* @returns An object containing the raw context window size and the effective max allowed size
|
||||
*/
|
||||
export function getContextWindowInfo(api: ApiHandler) {
|
||||
let contextWindow = api.getModel().info.contextWindow || 128_000
|
||||
// FIXME: hack to get anyone using openai compatible with deepseek to have the proper context window instead of the default 128k. We need a way for the user to specify the context window for models they input through openai compatible
|
||||
|
||||
// Handle special cases like DeepSeek
|
||||
if (api instanceof OpenAiHandler && api.getModel().id.toLowerCase().includes("deepseek")) {
|
||||
contextWindow = 64_000
|
||||
}
|
||||
|
||||
let maxAllowedSize: number
|
||||
switch (contextWindow) {
|
||||
case 64_000: // deepseek models
|
||||
maxAllowedSize = contextWindow - 27_000
|
||||
break
|
||||
case 128_000: // most models
|
||||
maxAllowedSize = contextWindow - 30_000
|
||||
break
|
||||
case 200_000: // claude models
|
||||
maxAllowedSize = contextWindow - 40_000
|
||||
break
|
||||
default:
|
||||
maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8) // for deepseek, 80% of 64k meant only ~10k buffer which was too small and resulted in users getting context window errors.
|
||||
}
|
||||
|
||||
return { contextWindow, maxAllowedSize }
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export class Controller {
|
||||
workspaceTracker?: WorkspaceTracker
|
||||
mcpHub?: McpHub
|
||||
accountService?: ClineAccountService
|
||||
private latestAnnouncementId = "march-22-2025" // update to some unique identifier when we add a new announcement
|
||||
private latestAnnouncementId = "april-7-2025" // update to some unique identifier when we add a new announcement
|
||||
private webviewProviderRef: WeakRef<WebviewProvider>
|
||||
|
||||
constructor(
|
||||
@@ -597,7 +597,14 @@ export class Controller {
|
||||
await this.togglePlanActModeWithChatSettings({ mode: "act" })
|
||||
}
|
||||
|
||||
// 2. download MCP
|
||||
// 2. Enable MCP settings if disabled
|
||||
// Enable MCP mode if disabled
|
||||
const mcpConfig = vscode.workspace.getConfiguration("cline.mcp")
|
||||
if (mcpConfig.get<string>("mode") !== "full") {
|
||||
await mcpConfig.update("mode", "full", true)
|
||||
}
|
||||
|
||||
// 3. download MCP
|
||||
await this.downloadMcp(message.mcpId)
|
||||
}
|
||||
break
|
||||
@@ -1359,7 +1366,6 @@ export class Controller {
|
||||
|
||||
// Create task with context from README and added guidelines for MCP server installation
|
||||
const task = `Set up the MCP server from ${mcpDetails.githubUrl} while adhering to these MCP server installation rules:
|
||||
- Start by loading the MCP documentation.
|
||||
- Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
|
||||
- Create the directory for the new MCP server before starting installation.
|
||||
- Use commands aligned with the user's shell and operating system best practices.
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
|
||||
export async function loadMcpDocumentation(mcpHub: McpHub) {
|
||||
return `## Creating an MCP Server
|
||||
|
||||
When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through obtaining an application client ID and secret, you may have to create a separate one-time setup script (like get-refresh-token.js) that captures and logs the final piece of the puzzle: the user's refresh token (i.e. you might run the script using execute_command which would open a browser for authentication, and then log the refresh token so that you can see it in the command output for you to use in the MCP settings configuration).
|
||||
|
||||
Unless the user specifies otherwise, new MCP servers should be created in: ${await mcpHub.getMcpServersPath()}
|
||||
|
||||
### Example MCP Server
|
||||
|
||||
For example, if the user wanted to give you the ability to retrieve weather information, you could create an MCP server that uses the OpenWeather API to get weather information, add it to the MCP settings configuration file, and then notice that you now have access to new tools and resources in the system prompt that you might use to show the user your new capabilities.
|
||||
|
||||
The following example demonstrates how to build an MCP server that provides weather data functionality. While this example shows how to implement resources, resource templates, and tools, in practice you should prefer using tools since they are more flexible and can handle dynamic parameters. The resource and resource template implementations are included here mainly for demonstration purposes of the different MCP capabilities, but a real weather server would likely just expose tools for fetching weather data. (The following steps are for macOS)
|
||||
|
||||
1. Use the \`create-typescript-server\` tool to bootstrap a new project in the default MCP servers directory:
|
||||
|
||||
\`\`\`bash
|
||||
cd ${await mcpHub.getMcpServersPath()}
|
||||
npx @modelcontextprotocol/create-server weather-server
|
||||
cd weather-server
|
||||
# Install dependencies
|
||||
npm install axios
|
||||
\`\`\`
|
||||
|
||||
This will create a new project with the following structure:
|
||||
|
||||
\`\`\`
|
||||
weather-server/
|
||||
├── package.json
|
||||
{
|
||||
...
|
||||
"type": "module", // added by default, uses ES module syntax (import/export) rather than CommonJS (require/module.exports) (Important to know if you create additional scripts in this server repository like a get-refresh-token.js script)
|
||||
"scripts": {
|
||||
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
├── tsconfig.json
|
||||
└── src/
|
||||
└── weather-server/
|
||||
└── index.ts # Main server implementation
|
||||
\`\`\`
|
||||
|
||||
2. Replace \`src/index.ts\` with the following:
|
||||
|
||||
\`\`\`typescript
|
||||
#!/usr/bin/env node
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ErrorCode,
|
||||
ListResourcesRequestSchema,
|
||||
ListResourceTemplatesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
McpError,
|
||||
ReadResourceRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_KEY = process.env.OPENWEATHER_API_KEY; // provided by MCP config
|
||||
if (!API_KEY) {
|
||||
throw new Error('OPENWEATHER_API_KEY environment variable is required');
|
||||
}
|
||||
|
||||
interface OpenWeatherResponse {
|
||||
main: {
|
||||
temp: number;
|
||||
humidity: number;
|
||||
};
|
||||
weather: [{ description: string }];
|
||||
wind: { speed: number };
|
||||
dt_txt?: string;
|
||||
}
|
||||
|
||||
const isValidForecastArgs = (
|
||||
args: any
|
||||
): args is { city: string; days?: number } =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.city === 'string' &&
|
||||
(args.days === undefined || typeof args.days === 'number');
|
||||
|
||||
class WeatherServer {
|
||||
private server: Server;
|
||||
private axiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'example-weather-server',
|
||||
version: '0.1.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.axiosInstance = axios.create({
|
||||
baseURL: 'http://api.openweathermap.org/data/2.5',
|
||||
params: {
|
||||
appid: API_KEY,
|
||||
units: 'metric',
|
||||
},
|
||||
});
|
||||
|
||||
this.setupResourceHandlers();
|
||||
this.setupToolHandlers();
|
||||
|
||||
// Error handling
|
||||
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
||||
process.on('SIGINT', async () => {
|
||||
await this.server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// MCP Resources represent any kind of UTF-8 encoded data that an MCP server wants to make available to clients, such as database records, API responses, log files, and more. Servers define direct resources with a static URI or dynamic resources with a URI template that follows the format \`[protocol]://[host]/[path]\`.
|
||||
private setupResourceHandlers() {
|
||||
// For static resources, servers can expose a list of resources:
|
||||
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
||||
resources: [
|
||||
// This is a poor example since you could use the resource template to get the same information but this demonstrates how to define a static resource
|
||||
{
|
||||
uri: \`weather://San Francisco/current\`, // Unique identifier for San Francisco weather resource
|
||||
name: \`Current weather in San Francisco\`, // Human-readable name
|
||||
mimeType: 'application/json', // Optional MIME type
|
||||
// Optional description
|
||||
description:
|
||||
'Real-time weather data for San Francisco including temperature, conditions, humidity, and wind speed',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// For dynamic resources, servers can expose resource templates:
|
||||
this.server.setRequestHandler(
|
||||
ListResourceTemplatesRequestSchema,
|
||||
async () => ({
|
||||
resourceTemplates: [
|
||||
{
|
||||
uriTemplate: 'weather://{city}/current', // URI template (RFC 6570)
|
||||
name: 'Current weather for a given city', // Human-readable name
|
||||
mimeType: 'application/json', // Optional MIME type
|
||||
description: 'Real-time weather data for a specified city', // Optional description
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// ReadResourceRequestSchema is used for both static resources and dynamic resource templates
|
||||
this.server.setRequestHandler(
|
||||
ReadResourceRequestSchema,
|
||||
async (request) => {
|
||||
const match = request.params.uri.match(
|
||||
/^weather:\/\/([^/]+)\/current$/
|
||||
);
|
||||
if (!match) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
\`Invalid URI format: \${request.params.uri}\`
|
||||
);
|
||||
}
|
||||
const city = decodeURIComponent(match[1]);
|
||||
|
||||
try {
|
||||
const response = await this.axiosInstance.get(
|
||||
'weather', // current weather
|
||||
{
|
||||
params: { q: city },
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: request.params.uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
temperature: response.data.main.temp,
|
||||
conditions: response.data.weather[0].description,
|
||||
humidity: response.data.main.humidity,
|
||||
wind_speed: response.data.wind.speed,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
\`Weather API error: \${
|
||||
error.response?.data.message ?? error.message
|
||||
}\`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* MCP Tools enable servers to expose executable functionality to the system. Through these tools, you can interact with external systems, perform computations, and take actions in the real world.
|
||||
* - Like resources, tools are identified by unique names and can include descriptions to guide their usage. However, unlike resources, tools represent dynamic operations that can modify state or interact with external systems.
|
||||
* - While resources and tools are similar, you should prefer to create tools over resources when possible as they provide more flexibility.
|
||||
*/
|
||||
private setupToolHandlers() {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: 'get_forecast', // Unique identifier
|
||||
description: 'Get weather forecast for a city', // Human-readable description
|
||||
inputSchema: {
|
||||
// JSON Schema for parameters
|
||||
type: 'object',
|
||||
properties: {
|
||||
city: {
|
||||
type: 'string',
|
||||
description: 'City name',
|
||||
},
|
||||
days: {
|
||||
type: 'number',
|
||||
description: 'Number of days (1-5)',
|
||||
minimum: 1,
|
||||
maximum: 5,
|
||||
},
|
||||
},
|
||||
required: ['city'], // Array of required property names
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name !== 'get_forecast') {
|
||||
throw new McpError(
|
||||
ErrorCode.MethodNotFound,
|
||||
\`Unknown tool: \${request.params.name}\`
|
||||
);
|
||||
}
|
||||
|
||||
if (!isValidForecastArgs(request.params.arguments)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid forecast arguments'
|
||||
);
|
||||
}
|
||||
|
||||
const city = request.params.arguments.city;
|
||||
const days = Math.min(request.params.arguments.days || 3, 5);
|
||||
|
||||
try {
|
||||
const response = await this.axiosInstance.get<{
|
||||
list: OpenWeatherResponse[];
|
||||
}>('forecast', {
|
||||
params: {
|
||||
q: city,
|
||||
cnt: days * 8,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response.data.list, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: \`Weather API error: \${
|
||||
error.response?.data.message ?? error.message
|
||||
}\`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error('Weather MCP server running on stdio');
|
||||
}
|
||||
}
|
||||
|
||||
const server = new WeatherServer();
|
||||
server.run().catch(console.error);
|
||||
\`\`\`
|
||||
|
||||
(Remember: This is just an example–you may use different dependencies, break the implementation up into multiple files, etc.)
|
||||
|
||||
3. Build and compile the executable JavaScript file
|
||||
|
||||
\`\`\`bash
|
||||
npm run build
|
||||
\`\`\`
|
||||
|
||||
4. Whenever you need an environment variable such as an API key to configure the MCP server, walk the user through the process of getting the key. For example, they may need to create an account and go to a developer dashboard to generate the key. Provide step-by-step instructions and URLs to make it easy for the user to retrieve the necessary information. Then use the ask_followup_question tool to ask the user for the key, in this case the OpenWeather API key.
|
||||
|
||||
5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object.
|
||||
|
||||
IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false and autoApprove=[].
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"mcpServers": {
|
||||
...,
|
||||
"weather": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/weather-server/build/index.js"],
|
||||
"env": {
|
||||
"OPENWEATHER_API_KEY": "user-provided-api-key"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify \`~/Library/Application\ Support/Claude/claude_desktop_config.json\` on macOS for example. It follows the same format of a top level \`mcpServers\` object.)
|
||||
|
||||
6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section. (Note: If you encounter a 'not connected' error when testing a newly installed mcp server, a common cause is an incorrect build path in your MCP settings configuration. Since compiled JavaScript files are commonly output to either 'dist/' or 'build/' directories, double-check that the build path in your MCP settings matches where your files are actually being compiled. E.g. If you assumed 'build' as the folder, check tsconfig.json to see if it's using 'dist' instead.)
|
||||
|
||||
7. Now that you have access to these new tools and resources, you may suggest ways the user can command you to invoke them - for example, with this new weather tool now available, you can invite the user to ask "what's the weather in San Francisco?"
|
||||
|
||||
## Editing MCP Servers
|
||||
|
||||
The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' below: ${
|
||||
mcpHub
|
||||
.getServers()
|
||||
.filter((server) => server.status === "connected")
|
||||
.map((server) => server.name)
|
||||
.join(", ") || "(None running currently)"
|
||||
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use replace_in_file to make changes to the files.
|
||||
|
||||
However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server.
|
||||
|
||||
# MCP Servers Are Not Always Necessary
|
||||
|
||||
The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that...").
|
||||
|
||||
Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.`
|
||||
}
|
||||
+418
-3
@@ -177,6 +177,9 @@ Usage:
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
mcpHub.getMode() !== "off"
|
||||
? `
|
||||
## use_mcp_tool
|
||||
Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.
|
||||
Parameters:
|
||||
@@ -205,6 +208,9 @@ Usage:
|
||||
<server_name>server name here</server_name>
|
||||
<uri>resource URI here</uri>
|
||||
</access_mcp_resource>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
## ask_followup_question
|
||||
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
|
||||
@@ -233,6 +239,20 @@ Your final result description here
|
||||
<command>Command to demonstrate result (optional)</command>
|
||||
</attempt_completion>
|
||||
|
||||
## new_task
|
||||
Description: Request to create a new task with preloaded context. The user will be presented with a preview of the context and can choose to create a new task or keep chatting in the current conversation. The user may choose to start a new task at any point.
|
||||
Parameters:
|
||||
- context: (required) The context to preload the new task with. This should include:
|
||||
* Comprehensively explain what has been accomplished in the current task - mention specific file names that are relevant
|
||||
* The specific next steps or focus for the new task - mention specific file names that are relevant
|
||||
* Any critical information needed to continue the work
|
||||
* Clear indication of how this new task relates to the overall workflow
|
||||
* This should be akin to a long handoff file, enough for a totally new developer to be able to pick up where you left off and know exactly what to do next and which files to look at.
|
||||
Usage:
|
||||
<new_task>
|
||||
<context>context to preload new task with</context>
|
||||
</new_task>
|
||||
|
||||
## plan_mode_respond
|
||||
Description: Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should be used when you need to provide a response to a question or statement from the user about how you plan to accomplish the task. This tool is only available in PLAN MODE. The environment_details will specify the current mode, if it is not PLAN MODE then you should not use this tool. Depending on the user's message, you may ask questions to get clarification about the user's request, architect a solution to the task, and to brainstorm ideas with the user. For example, if the user's task is to create a website, you may start by asking some clarifying questions, then present a detailed plan for how you will accomplish the task given the context, and perhaps engage in a back and forth to finalize the details before the user switches you to ACT MODE to implement the solution.
|
||||
Parameters:
|
||||
@@ -318,6 +338,9 @@ return (
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
</replace_in_file>
|
||||
${
|
||||
mcpHub.getMode() !== "off"
|
||||
? `
|
||||
|
||||
## Example 4: Requesting to use an MCP tool
|
||||
|
||||
@@ -332,7 +355,14 @@ return (
|
||||
</arguments>
|
||||
</use_mcp_tool>
|
||||
|
||||
## Example 5: Another example of using an MCP tool (where the server name is a unique identifier such as a URL)
|
||||
## Example 5: Requesting to access an MCP resource
|
||||
|
||||
<access_mcp_resource>
|
||||
<server_name>weather-server</server_name>
|
||||
<uri>weather://san-francisco/current</uri>
|
||||
</access_mcp_resource>
|
||||
|
||||
## Example 6: Another example of using an MCP tool (where the server name is a unique identifier such as a URL)
|
||||
|
||||
<use_mcp_tool>
|
||||
<server_name>github.com/modelcontextprotocol/servers/tree/main/src/github</server_name>
|
||||
@@ -347,7 +377,9 @@ return (
|
||||
"assignees": ["octocat"]
|
||||
}
|
||||
</arguments>
|
||||
</use_mcp_tool>
|
||||
</use_mcp_tool>`
|
||||
: ""
|
||||
}
|
||||
|
||||
# Tool Use Guidelines
|
||||
|
||||
@@ -370,6 +402,9 @@ It is crucial to proceed step-by-step, waiting for the user's message after each
|
||||
|
||||
By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
|
||||
|
||||
${
|
||||
mcpHub.getMode() !== "off"
|
||||
? `
|
||||
====
|
||||
|
||||
MCP SERVERS
|
||||
@@ -416,6 +451,374 @@ ${
|
||||
})
|
||||
.join("\n\n")}`
|
||||
: "(No MCP servers currently connected)"
|
||||
}`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
mcpHub.getMode() === "full"
|
||||
? `
|
||||
## Creating an MCP Server
|
||||
|
||||
The user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with \`use_mcp_tool\` and \`access_mcp_resource\`.
|
||||
|
||||
When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through obtaining an application client ID and secret, you may have to create a separate one-time setup script (like get-refresh-token.js) that captures and logs the final piece of the puzzle: the user's refresh token (i.e. you might run the script using execute_command which would open a browser for authentication, and then log the refresh token so that you can see it in the command output for you to use in the MCP settings configuration).
|
||||
|
||||
Unless the user specifies otherwise, new MCP servers should be created in: ${await mcpHub.getMcpServersPath()}
|
||||
|
||||
### Example MCP Server
|
||||
|
||||
For example, if the user wanted to give you the ability to retrieve weather information, you could create an MCP server that uses the OpenWeather API to get weather information, add it to the MCP settings configuration file, and then notice that you now have access to new tools and resources in the system prompt that you might use to show the user your new capabilities.
|
||||
|
||||
The following example demonstrates how to build an MCP server that provides weather data functionality. While this example shows how to implement resources, resource templates, and tools, in practice you should prefer using tools since they are more flexible and can handle dynamic parameters. The resource and resource template implementations are included here mainly for demonstration purposes of the different MCP capabilities, but a real weather server would likely just expose tools for fetching weather data. (The following steps are for macOS)
|
||||
|
||||
1. Use the \`create-typescript-server\` tool to bootstrap a new project in the default MCP servers directory:
|
||||
|
||||
\`\`\`bash
|
||||
cd ${await mcpHub.getMcpServersPath()}
|
||||
npx @modelcontextprotocol/create-server weather-server
|
||||
cd weather-server
|
||||
# Install dependencies
|
||||
npm install axios
|
||||
\`\`\`
|
||||
|
||||
This will create a new project with the following structure:
|
||||
|
||||
\`\`\`
|
||||
weather-server/
|
||||
├── package.json
|
||||
{
|
||||
...
|
||||
"type": "module", // added by default, uses ES module syntax (import/export) rather than CommonJS (require/module.exports) (Important to know if you create additional scripts in this server repository like a get-refresh-token.js script)
|
||||
"scripts": {
|
||||
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
├── tsconfig.json
|
||||
└── src/
|
||||
└── weather-server/
|
||||
└── index.ts # Main server implementation
|
||||
\`\`\`
|
||||
|
||||
2. Replace \`src/index.ts\` with the following:
|
||||
|
||||
\`\`\`typescript
|
||||
#!/usr/bin/env node
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ErrorCode,
|
||||
ListResourcesRequestSchema,
|
||||
ListResourceTemplatesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
McpError,
|
||||
ReadResourceRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_KEY = process.env.OPENWEATHER_API_KEY; // provided by MCP config
|
||||
if (!API_KEY) {
|
||||
throw new Error('OPENWEATHER_API_KEY environment variable is required');
|
||||
}
|
||||
|
||||
interface OpenWeatherResponse {
|
||||
main: {
|
||||
temp: number;
|
||||
humidity: number;
|
||||
};
|
||||
weather: [{ description: string }];
|
||||
wind: { speed: number };
|
||||
dt_txt?: string;
|
||||
}
|
||||
|
||||
const isValidForecastArgs = (
|
||||
args: any
|
||||
): args is { city: string; days?: number } =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.city === 'string' &&
|
||||
(args.days === undefined || typeof args.days === 'number');
|
||||
|
||||
class WeatherServer {
|
||||
private server: Server;
|
||||
private axiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'example-weather-server',
|
||||
version: '0.1.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.axiosInstance = axios.create({
|
||||
baseURL: 'http://api.openweathermap.org/data/2.5',
|
||||
params: {
|
||||
appid: API_KEY,
|
||||
units: 'metric',
|
||||
},
|
||||
});
|
||||
|
||||
this.setupResourceHandlers();
|
||||
this.setupToolHandlers();
|
||||
|
||||
// Error handling
|
||||
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
||||
process.on('SIGINT', async () => {
|
||||
await this.server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// MCP Resources represent any kind of UTF-8 encoded data that an MCP server wants to make available to clients, such as database records, API responses, log files, and more. Servers define direct resources with a static URI or dynamic resources with a URI template that follows the format \`[protocol]://[host]/[path]\`.
|
||||
private setupResourceHandlers() {
|
||||
// For static resources, servers can expose a list of resources:
|
||||
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
||||
resources: [
|
||||
// This is a poor example since you could use the resource template to get the same information but this demonstrates how to define a static resource
|
||||
{
|
||||
uri: \`weather://San Francisco/current\`, // Unique identifier for San Francisco weather resource
|
||||
name: \`Current weather in San Francisco\`, // Human-readable name
|
||||
mimeType: 'application/json', // Optional MIME type
|
||||
// Optional description
|
||||
description:
|
||||
'Real-time weather data for San Francisco including temperature, conditions, humidity, and wind speed',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// For dynamic resources, servers can expose resource templates:
|
||||
this.server.setRequestHandler(
|
||||
ListResourceTemplatesRequestSchema,
|
||||
async () => ({
|
||||
resourceTemplates: [
|
||||
{
|
||||
uriTemplate: 'weather://{city}/current', // URI template (RFC 6570)
|
||||
name: 'Current weather for a given city', // Human-readable name
|
||||
mimeType: 'application/json', // Optional MIME type
|
||||
description: 'Real-time weather data for a specified city', // Optional description
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// ReadResourceRequestSchema is used for both static resources and dynamic resource templates
|
||||
this.server.setRequestHandler(
|
||||
ReadResourceRequestSchema,
|
||||
async (request) => {
|
||||
const match = request.params.uri.match(
|
||||
/^weather:\/\/([^/]+)\/current$/
|
||||
);
|
||||
if (!match) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
\`Invalid URI format: \${request.params.uri}\`
|
||||
);
|
||||
}
|
||||
const city = decodeURIComponent(match[1]);
|
||||
|
||||
try {
|
||||
const response = await this.axiosInstance.get(
|
||||
'weather', // current weather
|
||||
{
|
||||
params: { q: city },
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: request.params.uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
temperature: response.data.main.temp,
|
||||
conditions: response.data.weather[0].description,
|
||||
humidity: response.data.main.humidity,
|
||||
wind_speed: response.data.wind.speed,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
\`Weather API error: \${
|
||||
error.response?.data.message ?? error.message
|
||||
}\`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* MCP Tools enable servers to expose executable functionality to the system. Through these tools, you can interact with external systems, perform computations, and take actions in the real world.
|
||||
* - Like resources, tools are identified by unique names and can include descriptions to guide their usage. However, unlike resources, tools represent dynamic operations that can modify state or interact with external systems.
|
||||
* - While resources and tools are similar, you should prefer to create tools over resources when possible as they provide more flexibility.
|
||||
*/
|
||||
private setupToolHandlers() {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: 'get_forecast', // Unique identifier
|
||||
description: 'Get weather forecast for a city', // Human-readable description
|
||||
inputSchema: {
|
||||
// JSON Schema for parameters
|
||||
type: 'object',
|
||||
properties: {
|
||||
city: {
|
||||
type: 'string',
|
||||
description: 'City name',
|
||||
},
|
||||
days: {
|
||||
type: 'number',
|
||||
description: 'Number of days (1-5)',
|
||||
minimum: 1,
|
||||
maximum: 5,
|
||||
},
|
||||
},
|
||||
required: ['city'], // Array of required property names
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name !== 'get_forecast') {
|
||||
throw new McpError(
|
||||
ErrorCode.MethodNotFound,
|
||||
\`Unknown tool: \${request.params.name}\`
|
||||
);
|
||||
}
|
||||
|
||||
if (!isValidForecastArgs(request.params.arguments)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid forecast arguments'
|
||||
);
|
||||
}
|
||||
|
||||
const city = request.params.arguments.city;
|
||||
const days = Math.min(request.params.arguments.days || 3, 5);
|
||||
|
||||
try {
|
||||
const response = await this.axiosInstance.get<{
|
||||
list: OpenWeatherResponse[];
|
||||
}>('forecast', {
|
||||
params: {
|
||||
q: city,
|
||||
cnt: days * 8,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response.data.list, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: \`Weather API error: \${
|
||||
error.response?.data.message ?? error.message
|
||||
}\`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error('Weather MCP server running on stdio');
|
||||
}
|
||||
}
|
||||
|
||||
const server = new WeatherServer();
|
||||
server.run().catch(console.error);
|
||||
\`\`\`
|
||||
|
||||
(Remember: This is just an example–you may use different dependencies, break the implementation up into multiple files, etc.)
|
||||
|
||||
3. Build and compile the executable JavaScript file
|
||||
|
||||
\`\`\`bash
|
||||
npm run build
|
||||
\`\`\`
|
||||
|
||||
4. Whenever you need an environment variable such as an API key to configure the MCP server, walk the user through the process of getting the key. For example, they may need to create an account and go to a developer dashboard to generate the key. Provide step-by-step instructions and URLs to make it easy for the user to retrieve the necessary information. Then use the ask_followup_question tool to ask the user for the key, in this case the OpenWeather API key.
|
||||
|
||||
5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object.
|
||||
|
||||
IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false and autoApprove=[].
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"mcpServers": {
|
||||
...,
|
||||
"weather": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/weather-server/build/index.js"],
|
||||
"env": {
|
||||
"OPENWEATHER_API_KEY": "user-provided-api-key"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify \`~/Library/Application\ Support/Claude/claude_desktop_config.json\` on macOS for example. It follows the same format of a top level \`mcpServers\` object.)
|
||||
|
||||
6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section. (Note: If you encounter a 'not connected' error when testing a newly installed mcp server, a common cause is an incorrect build path in your MCP settings configuration. Since compiled JavaScript files are commonly output to either 'dist/' or 'build/' directories, double-check that the build path in your MCP settings matches where your files are actually being compiled. E.g. If you assumed 'build' as the folder, check tsconfig.json to see if it's using 'dist' instead.)
|
||||
|
||||
7. Now that you have access to these new tools and resources, you may suggest ways the user can command you to invoke them - for example, with this new weather tool now available, you can invite the user to ask "what's the weather in San Francisco?"
|
||||
|
||||
## Editing MCP Servers
|
||||
|
||||
The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' below: ${
|
||||
mcpHub
|
||||
.getServers()
|
||||
.filter((server) => server.status === "connected")
|
||||
.map((server) => server.name)
|
||||
.join(", ") || "(None running currently)"
|
||||
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use replace_in_file to make changes to the files.
|
||||
|
||||
However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server.
|
||||
|
||||
# MCP Servers Are Not Always Necessary
|
||||
|
||||
The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that...").
|
||||
|
||||
Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
====
|
||||
@@ -530,7 +933,13 @@ CAPABILITIES
|
||||
? "\n- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.\n - For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser."
|
||||
: ""
|
||||
}
|
||||
${
|
||||
mcpHub.getMode() !== "off"
|
||||
? `
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
====
|
||||
|
||||
@@ -551,7 +960,7 @@ RULES
|
||||
- The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it.
|
||||
- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.${
|
||||
supportsComputerUse
|
||||
? `\n- The user may ask generic non-development tasks, such as "what\'s the latest news" or "look up the weather in San Diego", in which case you might use the browser_action tool to complete the task if it makes sense to do so, rather than trying to create a website or using curl to answer the question. However, if an available MCP server tool or resource can be used instead, you should prefer to use it over browser_action.`
|
||||
? `\n- The user may ask generic non-development tasks, such as "what\'s the latest news" or "look up the weather in San Diego", in which case you might use the browser_action tool to complete the task if it makes sense to do so, rather than trying to create a website or using curl to answer the question.${mcpHub.getMode() !== "off" ? "However, if an available MCP server tool or resource can be used instead, you should prefer to use it over browser_action." : ""}`
|
||||
: ""
|
||||
}
|
||||
- NEVER end attempt_completion result with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user.
|
||||
@@ -566,7 +975,13 @@ RULES
|
||||
? " Then if you want to test your work, you might use browser_action to launch the site, wait for the user's response confirming the site was launched along with a screenshot, then perhaps e.g., click a button to test functionality if needed, wait for the user's response confirming the button was clicked along with a screenshot of the new state, before finally closing the browser."
|
||||
: ""
|
||||
}
|
||||
${
|
||||
mcpHub.getMode() !== "off"
|
||||
? `
|
||||
- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
====
|
||||
|
||||
|
||||
+102
-27
@@ -13,6 +13,7 @@ import { ApiHandler, buildApiHandler } from "../../api"
|
||||
import { AnthropicHandler } from "../../api/providers/anthropic"
|
||||
import { ClineHandler } from "../../api/providers/cline"
|
||||
import { OpenRouterHandler } from "../../api/providers/openrouter"
|
||||
import { getContextWindowInfo } from "../context-management/context-window-utils"
|
||||
import { ApiStream } from "../../api/transform/stream"
|
||||
import CheckpointTracker from "../../integrations/checkpoints/CheckpointTracker"
|
||||
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../../integrations/editor/DiffViewProvider"
|
||||
@@ -78,7 +79,6 @@ import {
|
||||
saveClineMessages,
|
||||
GlobalFileNames,
|
||||
} from "../storage/disk"
|
||||
import { loadMcpDocumentation } from "../prompts/loadMcpDocumentation"
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
|
||||
|
||||
@@ -1471,10 +1471,10 @@ export class Task {
|
||||
return `[${block.name} for '${block.params.question}']`
|
||||
case "plan_mode_respond":
|
||||
return `[${block.name}]`
|
||||
case "load_mcp_documentation":
|
||||
return `[${block.name}]`
|
||||
case "attempt_completion":
|
||||
return `[${block.name}]`
|
||||
case "new_task":
|
||||
return `[${block.name} for creating a new task]`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1957,12 +1957,34 @@ export class Task {
|
||||
// Track file read operation
|
||||
await this.fileContextTracker.trackFileContext(relPath, "read_tool")
|
||||
|
||||
pushToolResult(content)
|
||||
// Format the result as XML
|
||||
let xmlResult: string
|
||||
if (content === "") {
|
||||
// Empty file case
|
||||
xmlResult = `<file><path>${relPath}</path><content/><notice>File is empty</notice></file>`
|
||||
} else {
|
||||
// Normal file with content
|
||||
xmlResult = `<file><path>${relPath}</path><content>\n${content}\n</content></file>`
|
||||
}
|
||||
|
||||
pushToolResult(xmlResult)
|
||||
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("reading file", error)
|
||||
const errorMessage = error instanceof Error ? error.message : JSON.stringify(serializeError(error))
|
||||
// Show error in UI (similar to what handleError would do)
|
||||
await this.say(
|
||||
"error",
|
||||
`Error reading file:\n${errorMessage ?? JSON.stringify(serializeError(error), null, 2)}`,
|
||||
)
|
||||
|
||||
// Ensure relPath is defined for the XML, even in error cases
|
||||
const safeRelPath = relPath || "unknown_path"
|
||||
// Format error as XML without content tag
|
||||
const errorXml = `<file><path>${safeRelPath}</path><error>${errorMessage}</error></file>`
|
||||
|
||||
pushToolResult(errorXml)
|
||||
|
||||
break
|
||||
}
|
||||
@@ -2743,6 +2765,51 @@ export class Task {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "new_task": {
|
||||
const context: string | undefined = block.params.context
|
||||
try {
|
||||
if (block.partial) {
|
||||
await this.ask("new_task", removeClosingTag("context", context), block.partial).catch(() => {})
|
||||
break
|
||||
} else {
|
||||
if (!context) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("new_task", "context"))
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
|
||||
if (this.autoApprovalSettings.enabled && this.autoApprovalSettings.enableNotifications) {
|
||||
showSystemNotification({
|
||||
subtitle: "Cline wants to start a new task...",
|
||||
message: `Cline is suggesting to start a new task with: ${context}`,
|
||||
})
|
||||
}
|
||||
|
||||
const { text, images } = await this.ask("new_task", context, false)
|
||||
|
||||
// If the user provided a response, treat it as feedback
|
||||
if (text || images?.length) {
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
pushToolResult(
|
||||
formatResponse.toolResult(
|
||||
`The user provided feedback instead of creating a new task:\n<feedback>\n${text}\n</feedback>`,
|
||||
images,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
// If no response, the user clicked the "Create New Task" button
|
||||
pushToolResult(
|
||||
formatResponse.toolResult(`The user has created a new task with the provided context.`),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("creating new task", error)
|
||||
break
|
||||
}
|
||||
}
|
||||
case "plan_mode_respond": {
|
||||
const response: string | undefined = block.params.response
|
||||
const optionsRaw: string | undefined = block.params.options
|
||||
@@ -2827,28 +2894,6 @@ export class Task {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "load_mcp_documentation": {
|
||||
try {
|
||||
if (block.partial) {
|
||||
// shouldn't happen
|
||||
break
|
||||
} else {
|
||||
await this.say("load_mcp_documentation", "", undefined, false)
|
||||
|
||||
const mcpHub = this.controllerRef.deref()?.mcpHub
|
||||
if (!mcpHub) {
|
||||
throw new Error("MCP hub not available")
|
||||
}
|
||||
|
||||
pushToolResult(await loadMcpDocumentation(mcpHub))
|
||||
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("loading MCP documentation", error)
|
||||
break
|
||||
}
|
||||
}
|
||||
case "attempt_completion": {
|
||||
/*
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -3628,6 +3673,36 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
// Add context window usage information
|
||||
const { contextWindow, maxAllowedSize } = getContextWindowInfo(this.api)
|
||||
|
||||
// Get the token count from the most recent API request to accurately reflect context management
|
||||
const getTotalTokensFromApiReqMessage = (msg: ClineMessage) => {
|
||||
if (!msg.text) {
|
||||
return 0
|
||||
}
|
||||
try {
|
||||
const { tokensIn, tokensOut, cacheWrites, cacheReads } = JSON.parse(msg.text)
|
||||
return (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
|
||||
} catch (e) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
const modifiedMessages = combineApiRequests(combineCommandSequences(this.clineMessages.slice(1)))
|
||||
const lastApiReqMessage = findLast(modifiedMessages, (msg) => {
|
||||
if (msg.say !== "api_req_started") {
|
||||
return false
|
||||
}
|
||||
return getTotalTokensFromApiReqMessage(msg) > 0
|
||||
})
|
||||
|
||||
const lastApiReqTotalTokens = lastApiReqMessage ? getTotalTokensFromApiReqMessage(lastApiReqMessage) : 0
|
||||
const usagePercentage = Math.round((lastApiReqTotalTokens / contextWindow) * 100)
|
||||
|
||||
details += "\n# Context Window Usage"
|
||||
details += `\n${lastApiReqTotalTokens.toLocaleString()} / ${(contextWindow / 1000).toLocaleString()}K tokens used (${usagePercentage}%)`
|
||||
|
||||
details += "\n\n# Current Mode"
|
||||
if (this.chatSettings.mode === "plan") {
|
||||
details += "\nPLAN MODE\n" + formatResponse.planModeInstructions()
|
||||
|
||||
@@ -94,6 +94,10 @@ export class McpHub {
|
||||
return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server)
|
||||
}
|
||||
|
||||
getMode(): McpMode {
|
||||
return vscode.workspace.getConfiguration("cline.mcp").get<McpMode>("mode", "full")
|
||||
}
|
||||
|
||||
async getMcpServersPath(): Promise<string> {
|
||||
const provider = this.controllerRef.deref()
|
||||
if (!provider) {
|
||||
|
||||
@@ -169,6 +169,7 @@ export type ClineAsk =
|
||||
| "auto_approval_max_req_reached"
|
||||
| "browser_action_launch"
|
||||
| "use_mcp_server"
|
||||
| "new_task"
|
||||
|
||||
export type ClineSay =
|
||||
| "task"
|
||||
@@ -195,7 +196,6 @@ export type ClineSay =
|
||||
| "deleted_api_reqs"
|
||||
| "clineignore_error"
|
||||
| "checkpoint_created"
|
||||
| "load_mcp_documentation"
|
||||
|
||||
export interface ClineSayTool {
|
||||
tool:
|
||||
@@ -256,6 +256,10 @@ export interface ClineAskQuestion {
|
||||
selected?: string
|
||||
}
|
||||
|
||||
export interface ClineAskNewTask {
|
||||
context: string
|
||||
}
|
||||
|
||||
export interface ClineApiReqInfo {
|
||||
request?: string
|
||||
tokensIn?: number
|
||||
|
||||
+27
-6
@@ -82,14 +82,21 @@ export type ApiConfiguration = ApiHandlerOptions & {
|
||||
|
||||
// Models
|
||||
|
||||
interface PriceTier {
|
||||
tokenLimit: number // Upper limit (inclusive) of *input* tokens for this price. Use Infinity for the highest tier.
|
||||
price: number // Price per million tokens for this tier.
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
maxTokens?: number
|
||||
contextWindow?: number
|
||||
supportsImages?: boolean
|
||||
supportsComputerUse?: boolean
|
||||
supportsPromptCache: boolean // this value is hardcoded for now
|
||||
inputPrice?: number
|
||||
outputPrice?: number
|
||||
inputPrice?: number // Keep for non-tiered input models
|
||||
inputPriceTiers?: PriceTier[] // Add for tiered input pricing
|
||||
outputPrice?: number // Keep for non-tiered output models
|
||||
outputPriceTiers?: PriceTier[] // Add for tiered output pricing
|
||||
cacheWritesPrice?: number
|
||||
cacheReadsPrice?: number
|
||||
description?: string
|
||||
@@ -384,8 +391,16 @@ export const vertexModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10,
|
||||
// inputPrice: 1.25, // Removed
|
||||
// outputPrice: 10, // Removed
|
||||
inputPriceTiers: [
|
||||
{ tokenLimit: 200000, price: 1.25 }, // Input price for <= 200k input tokens
|
||||
{ tokenLimit: Infinity, price: 2.5 }, // Input price for > 200k input tokens
|
||||
],
|
||||
outputPriceTiers: [
|
||||
{ tokenLimit: 200000, price: 10.0 }, // Output price for <= 200k input tokens
|
||||
{ tokenLimit: Infinity, price: 15.0 }, // Output price for > 200k input tokens
|
||||
],
|
||||
},
|
||||
"gemini-2.0-flash-thinking-exp-01-21": {
|
||||
maxTokens: 65_536,
|
||||
@@ -474,8 +489,14 @@ export const geminiModels = {
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10,
|
||||
inputPriceTiers: [
|
||||
{ tokenLimit: 200000, price: 1.25 }, // Input price for <= 200k input tokens
|
||||
{ tokenLimit: Infinity, price: 2.5 }, // Input price for > 200k input tokens
|
||||
],
|
||||
outputPriceTiers: [
|
||||
{ tokenLimit: 200000, price: 10.0 }, // Output price for <= 200k input tokens
|
||||
{ tokenLimit: Infinity, price: 15.0 }, // Output price for > 200k input tokens
|
||||
],
|
||||
},
|
||||
"gemini-2.0-flash-001": {
|
||||
maxTokens: 8192,
|
||||
|
||||
+49
-5
@@ -2,15 +2,48 @@ import { ModelInfo } from "../shared/api"
|
||||
|
||||
function calculateApiCostInternal(
|
||||
modelInfo: ModelInfo,
|
||||
inputTokens: number,
|
||||
inputTokens: number, // Note: For OpenAI-style, this is non-cached tokens. For Anthropic-style, this is total input tokens.
|
||||
outputTokens: number,
|
||||
cacheCreationInputTokens: number,
|
||||
cacheReadInputTokens: number,
|
||||
totalInputTokensForPricing?: number, // The *total* input tokens, used for tiered pricing lookup
|
||||
): number {
|
||||
// Determine effective input price
|
||||
let effectiveInputPrice = modelInfo.inputPrice || 0
|
||||
if (modelInfo.inputPriceTiers && modelInfo.inputPriceTiers.length > 0 && totalInputTokensForPricing !== undefined) {
|
||||
// Ensure tiers are sorted by tokenLimit ascending before finding
|
||||
const sortedInputTiers = [...modelInfo.inputPriceTiers].sort((a, b) => a.tokenLimit - b.tokenLimit)
|
||||
// Find the first tier where the total input tokens are less than or equal to the limit
|
||||
const tier = sortedInputTiers.find((t) => totalInputTokensForPricing! <= t.tokenLimit)
|
||||
if (tier) {
|
||||
effectiveInputPrice = tier.price
|
||||
} else {
|
||||
// Should ideally not happen if Infinity is used for the last tier, but fallback just in case
|
||||
effectiveInputPrice = sortedInputTiers[sortedInputTiers.length - 1]?.price || 0
|
||||
}
|
||||
}
|
||||
|
||||
// Determine effective output price (based on total *input* tokens for pricing)
|
||||
let effectiveOutputPrice = modelInfo.outputPrice || 0
|
||||
if (modelInfo.outputPriceTiers && modelInfo.outputPriceTiers.length > 0 && totalInputTokensForPricing !== undefined) {
|
||||
// Ensure tiers are sorted by tokenLimit ascending before finding
|
||||
const sortedOutputTiers = [...modelInfo.outputPriceTiers].sort((a, b) => a.tokenLimit - b.tokenLimit)
|
||||
const tier = sortedOutputTiers.find((t) => totalInputTokensForPricing! <= t.tokenLimit)
|
||||
if (tier) {
|
||||
effectiveOutputPrice = tier.price
|
||||
} else {
|
||||
// Should ideally not happen if Infinity is used for the last tier, but fallback just in case
|
||||
effectiveOutputPrice = sortedOutputTiers[sortedOutputTiers.length - 1]?.price || 0
|
||||
}
|
||||
}
|
||||
|
||||
const cacheWritesCost = ((modelInfo.cacheWritesPrice || 0) / 1_000_000) * cacheCreationInputTokens
|
||||
const cacheReadsCost = ((modelInfo.cacheReadsPrice || 0) / 1_000_000) * cacheReadInputTokens
|
||||
const baseInputCost = ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens
|
||||
const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens
|
||||
// Use effectiveInputPrice for baseInputCost. Note: 'inputTokens' here is the potentially adjusted count (e.g., non-cached for OpenAI)
|
||||
const baseInputCost = (effectiveInputPrice / 1_000_000) * inputTokens
|
||||
// Use effectiveOutputPrice for outputCost
|
||||
const outputCost = (effectiveOutputPrice / 1_000_000) * outputTokens
|
||||
|
||||
const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
|
||||
return totalCost
|
||||
}
|
||||
@@ -25,7 +58,15 @@ export function calculateApiCostAnthropic(
|
||||
): number {
|
||||
const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
|
||||
const cacheReadInputTokensNum = cacheReadInputTokens || 0
|
||||
return calculateApiCostInternal(modelInfo, inputTokens, outputTokens, cacheCreationInputTokensNum, cacheReadInputTokensNum)
|
||||
// Anthropic style doesn't need totalInputTokensForPricing as its inputTokens already represents the total
|
||||
return calculateApiCostInternal(
|
||||
modelInfo,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheCreationInputTokensNum,
|
||||
cacheReadInputTokensNum,
|
||||
undefined, // Pass undefined for totalInputTokensForPricing
|
||||
)
|
||||
}
|
||||
|
||||
// For OpenAI compliant usage, the input tokens count INCLUDES the cached tokens
|
||||
@@ -38,12 +79,15 @@ export function calculateApiCostOpenAI(
|
||||
): number {
|
||||
const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
|
||||
const cacheReadInputTokensNum = cacheReadInputTokens || 0
|
||||
// Calculate non-cached tokens for the internal function's 'inputTokens' parameter
|
||||
const nonCachedInputTokens = Math.max(0, inputTokens - cacheCreationInputTokensNum - cacheReadInputTokensNum)
|
||||
// Pass the original 'inputTokens' as 'totalInputTokensForPricing' for tier lookup
|
||||
return calculateApiCostInternal(
|
||||
modelInfo,
|
||||
nonCachedInputTokens,
|
||||
nonCachedInputTokens, // Pass the adjusted token count here
|
||||
outputTokens,
|
||||
cacheCreationInputTokensNum,
|
||||
cacheReadInputTokensNum,
|
||||
inputTokens, // Pass the original total input tokens for pricing tier lookup
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,20 +43,21 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
</h3>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Add to Cline:</b> Right-click selected text in any file or terminal to quickly add context to your current
|
||||
task! Plus, when you see a lightbulb icon, select 'Fix with Cline' to have Cline fix errors in your code.
|
||||
<b>Browser Tool Upgrades:</b> Use your local Chrome browser for session-based browsing, enabling debugging and
|
||||
productivity workflows tied to your actual browser state!
|
||||
</li>
|
||||
<li>
|
||||
<b>Billing Dashboard:</b> Track your remaining credits and transaction history right in the extension with a{" "}
|
||||
<span className="codicon codicon-account" style={accountIconStyle}></span> Cline account!
|
||||
<b>Auto-Approve Commands:</b> New option to automatically approve <b>ALL</b> commands (use at your own risk!)
|
||||
</li>
|
||||
<li>
|
||||
<b>Faster Inference:</b> Cline/OpenRouter users can sort underlying providers used by throughput, price, and
|
||||
latency. Sorting by throughput will output faster generations (at a higher cost).
|
||||
<b>Easily Toggle MCP's:</b> New modal in the chat area to easily enable/disable MCP servers.
|
||||
</li>
|
||||
<li>
|
||||
<b>Enhanced MCP Support:</b> Dynamic image loading with GIF support, and a new delete button to clean up
|
||||
failed servers.
|
||||
<b>Smarter Context Management:</b> When hitting context window limits, old file contents are removed
|
||||
first–preserving narrative integrity and reducing Cline getting stuck in loops.
|
||||
</li>
|
||||
<li>
|
||||
Drag and drop files/folders into chat by holding <code>Shift</code> while dragging it into the chat field.
|
||||
</li>
|
||||
</ul>
|
||||
{/*<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
|
||||
@@ -30,6 +30,7 @@ import { OptionsButtons } from "@/components/chat/OptionsButtons"
|
||||
import { highlightMentions } from "./TaskHeader"
|
||||
import SuccessButton from "@/components/common/SuccessButton"
|
||||
import TaskFeedbackButtons from "@/components/chat/TaskFeedbackButtons"
|
||||
import NewTaskPreview from "./NewTaskPreview"
|
||||
import McpResourceRow from "@/components/mcp/configuration/tabs/installed/server-row/McpResourceRow"
|
||||
|
||||
const ChatRowContainer = styled.div`
|
||||
@@ -989,21 +990,6 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
<CheckmarkControl messageTs={message.ts} isCheckpointCheckedOut={message.isCheckpointCheckedOut} />
|
||||
</>
|
||||
)
|
||||
case "load_mcp_documentation":
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "var(--vscode-foreground)",
|
||||
opacity: 0.7,
|
||||
fontSize: 12,
|
||||
padding: "4px 0",
|
||||
}}>
|
||||
<i className="codicon codicon-book" style={{ marginRight: 6 }} />
|
||||
Loading MCP documentation
|
||||
</div>
|
||||
)
|
||||
case "completion_result":
|
||||
const hasChanges = message.text?.endsWith(COMPLETION_RESULT_CHANGES_FLAG) ?? false
|
||||
const text = hasChanges ? message.text?.slice(0, -COMPLETION_RESULT_CHANGES_FLAG.length) : message.text
|
||||
@@ -1248,6 +1234,21 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
case "new_task":
|
||||
return (
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
<span
|
||||
className="codicon codicon-new-file"
|
||||
style={{
|
||||
color: normalColor,
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>Cline wants to start a new task:</span>
|
||||
</div>
|
||||
<NewTaskPreview context={message.text || ""} />
|
||||
</>
|
||||
)
|
||||
case "plan_mode_respond": {
|
||||
let response: string | undefined
|
||||
let options: string[] | undefined
|
||||
|
||||
@@ -195,6 +195,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
setSecondaryButtonText(undefined)
|
||||
setDidClickCancel(false)
|
||||
break
|
||||
case "new_task":
|
||||
setTextAreaDisabled(isPartial)
|
||||
setClineAsk("new_task")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Start New Task with Context")
|
||||
setSecondaryButtonText(undefined)
|
||||
break
|
||||
}
|
||||
break
|
||||
case "say":
|
||||
@@ -224,7 +231,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "mcp_server_response":
|
||||
case "completion_result":
|
||||
case "tool":
|
||||
case "load_mcp_documentation":
|
||||
break
|
||||
}
|
||||
break
|
||||
@@ -296,6 +302,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "resume_task":
|
||||
case "resume_completed_task":
|
||||
case "mistake_limit_reached":
|
||||
case "new_task": // user can provide feedback or reject the new task suggestion
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "messageResponse",
|
||||
@@ -361,6 +368,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
// extension waiting for feedback. but we can just present a new task button
|
||||
startNewTask()
|
||||
break
|
||||
case "new_task":
|
||||
console.info("new task button clicked!", { lastMessage, messages, clineAsk, text })
|
||||
vscode.postMessage({
|
||||
type: "newTask",
|
||||
text: lastMessage?.text,
|
||||
})
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
@@ -369,7 +383,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
// setSecondaryButtonText(undefined)
|
||||
disableAutoScrollRef.current = false
|
||||
},
|
||||
[clineAsk, startNewTask],
|
||||
[clineAsk, startNewTask, lastMessage],
|
||||
)
|
||||
|
||||
const handleSecondaryButtonClick = useCallback(
|
||||
@@ -457,12 +471,15 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "addToInput":
|
||||
setInputValue((prevValue) => {
|
||||
const newText = message.text ?? ""
|
||||
return prevValue ? `${prevValue}\n${newText}` : newText
|
||||
const newTextWithNewline = newText + "\n"
|
||||
return prevValue ? `${prevValue}\n${newTextWithNewline}` : newTextWithNewline
|
||||
})
|
||||
// Add scroll to bottom after state update
|
||||
// Auto focus the input and start the cursor on a new linefor easy typing
|
||||
setTimeout(() => {
|
||||
if (textAreaRef.current) {
|
||||
textAreaRef.current.scrollTop = textAreaRef.current.scrollHeight
|
||||
textAreaRef.current.focus()
|
||||
}
|
||||
}, 0)
|
||||
break
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from "react"
|
||||
import MarkdownBlock from "../common/MarkdownBlock"
|
||||
|
||||
interface NewTaskPreviewProps {
|
||||
context: string
|
||||
}
|
||||
|
||||
const NewTaskPreview: React.FC<NewTaskPreviewProps> = ({ context }) => {
|
||||
return (
|
||||
<div className="bg-[var(--vscode-badge-background)] text-[var(--vscode-badge-foreground)] rounded-[3px] p-[14px] pb-[6px]">
|
||||
<span style={{ fontWeight: "bold" }}>Task</span>
|
||||
<MarkdownBlock markdown={context} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewTaskPreview
|
||||
@@ -174,7 +174,10 @@ async function svgToPng(svgEl: SVGElement): Promise<string> {
|
||||
|
||||
const serializer = new XMLSerializer()
|
||||
const svgString = serializer.serializeToString(svgClone)
|
||||
const svgDataUrl = "data:image/svg+xml;base64," + btoa(decodeURIComponent(encodeURIComponent(svgString)))
|
||||
const encoder = new TextEncoder()
|
||||
const bytes = encoder.encode(svgString)
|
||||
const base64 = btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(""))
|
||||
const svgDataUrl = `data:image/svg+xml;base64,${base64}`
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
|
||||
@@ -1574,6 +1574,21 @@ export const formatPrice = (price: number) => {
|
||||
}).format(price)
|
||||
}
|
||||
|
||||
// Returns an array of formatted tier strings
|
||||
const formatTiers = (tiers: ModelInfo["inputPriceTiers"]): string[] => {
|
||||
if (!tiers || tiers.length === 0) {
|
||||
return []
|
||||
}
|
||||
return tiers.map((tier, index, arr) => {
|
||||
const prevLimit = index > 0 ? arr[index - 1].tokenLimit : 0
|
||||
const limitText =
|
||||
tier.tokenLimit === Infinity
|
||||
? `> ${prevLimit.toLocaleString()}` // Assumes sorted and Infinity is last
|
||||
: `<= ${tier.tokenLimit.toLocaleString()}`
|
||||
return `${formatPrice(tier.price)}/million tokens (${limitText} tokens)`
|
||||
})
|
||||
}
|
||||
|
||||
export const ModelInfoView = ({
|
||||
selectedModelId,
|
||||
modelInfo,
|
||||
@@ -1589,6 +1604,42 @@ export const ModelInfoView = ({
|
||||
}) => {
|
||||
const isGemini = Object.keys(geminiModels).includes(selectedModelId)
|
||||
|
||||
// Create elements for tiered pricing separately
|
||||
const inputPriceElement = modelInfo.inputPriceTiers ? (
|
||||
<Fragment key="inputPriceTiers">
|
||||
<span style={{ fontWeight: 500 }}>Input price:</span>
|
||||
<br />
|
||||
{formatTiers(modelInfo.inputPriceTiers).map((tierString, i, arr) => (
|
||||
<Fragment key={`inputTierFrag${i}`}>
|
||||
<span style={{ paddingLeft: "15px" }}>{tierString}</span>
|
||||
{i < arr.length - 1 && <br />}
|
||||
</Fragment>
|
||||
))}
|
||||
</Fragment>
|
||||
) : modelInfo.inputPrice !== undefined && modelInfo.inputPrice > 0 ? (
|
||||
<span key="inputPrice">
|
||||
<span style={{ fontWeight: 500 }}>Input price:</span> {formatPrice(modelInfo.inputPrice)}/million tokens
|
||||
</span>
|
||||
) : null
|
||||
|
||||
const outputPriceElement = modelInfo.outputPriceTiers ? (
|
||||
<Fragment key="outputPriceTiers">
|
||||
<span style={{ fontWeight: 500 }}>Output price:</span>
|
||||
<span style={{ fontStyle: "italic" }}> (based on input tokens)</span>
|
||||
<br />
|
||||
{formatTiers(modelInfo.outputPriceTiers).map((tierString, i, arr) => (
|
||||
<Fragment key={`outputTierFrag${i}`}>
|
||||
<span style={{ paddingLeft: "15px" }}>{tierString}</span>
|
||||
{i < arr.length - 1 && <br />}
|
||||
</Fragment>
|
||||
))}
|
||||
</Fragment>
|
||||
) : modelInfo.outputPrice !== undefined && modelInfo.outputPrice > 0 ? (
|
||||
<span key="outputPrice">
|
||||
<span style={{ fontWeight: 500 }}>Output price:</span> {formatPrice(modelInfo.outputPrice)}/million tokens
|
||||
</span>
|
||||
) : null
|
||||
|
||||
const infoItems = [
|
||||
modelInfo.description && (
|
||||
<ModelDescriptionMarkdown
|
||||
@@ -1624,11 +1675,7 @@ export const ModelInfoView = ({
|
||||
<span style={{ fontWeight: 500 }}>Max output:</span> {modelInfo.maxTokens?.toLocaleString()} tokens
|
||||
</span>
|
||||
),
|
||||
modelInfo.inputPrice !== undefined && modelInfo.inputPrice > 0 && (
|
||||
<span key="inputPrice">
|
||||
<span style={{ fontWeight: 500 }}>Input price:</span> {formatPrice(modelInfo.inputPrice)}/million tokens
|
||||
</span>
|
||||
),
|
||||
inputPriceElement, // Add the generated input price block
|
||||
modelInfo.supportsPromptCache && modelInfo.cacheWritesPrice && (
|
||||
<span key="cacheWritesPrice">
|
||||
<span style={{ fontWeight: 500 }}>Cache writes price:</span> {formatPrice(modelInfo.cacheWritesPrice || 0)}
|
||||
@@ -1641,11 +1688,7 @@ export const ModelInfoView = ({
|
||||
tokens
|
||||
</span>
|
||||
),
|
||||
modelInfo.outputPrice !== undefined && modelInfo.outputPrice > 0 && (
|
||||
<span key="outputPrice">
|
||||
<span style={{ fontWeight: 500 }}>Output price:</span> {formatPrice(modelInfo.outputPrice)}/million tokens
|
||||
</span>
|
||||
),
|
||||
outputPriceElement, // Add the generated output price block
|
||||
isGemini && (
|
||||
<span key="geminiInfo" style={{ fontStyle: "italic" }}>
|
||||
* Free up to {selectedModelId && selectedModelId.includes("flash") ? "15" : "2"} requests per minute. After that,
|
||||
|
||||
Reference in New Issue
Block a user