snapshot of https://github.com/mountain-loop/yaak.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Lint and Test / Lint/Test (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-23 17:39:41 +05:00
commit 906fb231cc
1544 changed files with 164131 additions and 0 deletions

1
plugins-external/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*/build

View File

@@ -0,0 +1,76 @@
# Yaak Faker Plugin
This is a template function that generates realistic fake data
for testing and development using [FakerJS](https://fakerjs.dev).
![CleanShot 2024-09-19 at 13 56 33@2x](https://github.com/user-attachments/assets/2f935110-4af2-4236-a50d-18db5454176d)
## Example JSON Body
Here's an example JSON body that uses fake data:
```json
{
"id": "${[ faker.string.uuid() ]}",
"name": "${[ faker.person.fullName() ]}",
"email": "${[ faker.internet.email() ]}",
"phone": "${[ faker.phone.number() ]}",
"address": {
"street": "${[ faker.location.streetAddress() ]}",
"city": "${[ faker.location.city() ]}",
"country": "${[ faker.location.country() ]}",
"zipCode": "${[ faker.location.zipCode() ]}"
},
"company": "${[ faker.company.name() ]}",
"website": "${[ faker.internet.url() ]}"
}
```
This will generate a random JSON body on every request:
```json
{
"id": "589f0aec-7310-4bf2-81c4-0b1bb7f1c3c1",
"name": "Lucy Gottlieb-Weissnat",
"email": "Destiny_Herzog@gmail.com",
"phone": "411.805.2871 x699",
"address": {
"street": "846 Christ Mills",
"city": "Spencerfurt",
"country": "United Kingdom",
"zipCode": "20354"
},
"company": "Emard, Kohler and Rutherford",
"website": "https://watery-detective.org"
}
```
## Available Categories
The plugin provides access to all FakerJS modules and their methods:
| Category | Description | Example Methods |
| ---------- | ------------------------- | ------------------------------------------ |
| `airline` | Airline-related data | `aircraftType`, `airline`, `airplane` |
| `animal` | Animal names and types | `bear`, `bird`, `cat`, `dog`, `fish` |
| `color` | Colors in various formats | `human`, `rgb`, `hex`, `hsl` |
| `commerce` | E-commerce data | `department`, `product`, `price` |
| `company` | Company information | `name`, `catchPhrase`, `bs` |
| `database` | Database-related data | `column`, `type`, `collation` |
| `date` | Date and time values | `recent`, `future`, `past`, `between` |
| `finance` | Financial data | `account`, `amount`, `currency` |
| `git` | Git-related data | `branch`, `commitEntry`, `commitSha` |
| `hacker` | Tech/hacker terminology | `abbreviation`, `noun`, `phrase` |
| `image` | Image URLs and data | `avatar`, `url`, `dataUri` |
| `internet` | Internet-related data | `email`, `url`, `ip`, `userAgent` |
| `location` | Geographic data | `city`, `country`, `latitude`, `longitude` |
| `lorem` | Lorem ipsum text | `word`, `sentence`, `paragraph` |
| `person` | Personal information | `firstName`, `lastName`, `fullName` |
| `music` | Music-related data | `genre`, `songName`, `artist` |
| `number` | Numeric data | `int`, `float`, `binary`, `hex` |
| `phone` | Phone numbers | `number`, `imei` |
| `science` | Scientific data | `chemicalElement`, `unit` |
| `string` | String utilities | `uuid`, `alpha`, `alphanumeric` |
| `system` | System-related data | `fileName`, `mimeType`, `fileExt` |
| `vehicle` | Vehicle information | `vehicle`, `manufacturer`, `model` |
| `word` | Word generation | `adjective`, `adverb`, `conjunction` |

View File

@@ -0,0 +1,24 @@
{
"name": "@yaak/faker",
"displayName": "Faker",
"version": "1.1.1",
"private": true,
"description": "Template functions for generating fake data using FakerJS",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins-external/faker"
},
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"test": "vp test --run tests"
},
"dependencies": {
"@faker-js/faker": "^10.5.0"
},
"devDependencies": {
"@types/node": "^25.0.3",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,106 @@
import { faker } from "@faker-js/faker";
import type { DynamicTemplateFunctionArg, PluginDefinition } from "@yaakapp/api";
const modules = [
"airline",
"animal",
"color",
"commerce",
"company",
"database",
"date",
"finance",
"git",
"hacker",
"image",
"internet",
"location",
"lorem",
"person",
"music",
"number",
"phone",
"science",
"string",
"system",
"vehicle",
"word",
];
function normalizeResult(result: unknown): string {
if (typeof result === "string") return result;
if (result instanceof Date) return result.toISOString();
return JSON.stringify(result);
}
// Whatever Yaak’s arg type shape is – rough example
function args(modName: string, fnName: string): DynamicTemplateFunctionArg[] {
return [
{
type: "banner",
color: "info",
inputs: [
{
type: "markdown",
content: `Need help? View documentation for [\`${modName}.${fnName}(…)\`](https://fakerjs.dev/api/${encodeURIComponent(modName)}.html#${encodeURIComponent(fnName)})`,
},
],
},
{
name: "options",
label: "Arguments",
type: "editor",
language: "json",
optional: true,
placeholder: 'e.g. { "min": 1, "max": 10 } or 10 or ["en","US"]',
},
];
}
export const plugin: PluginDefinition = {
templateFunctions: modules.flatMap((modName) => {
const mod = faker[modName as keyof typeof faker];
return Object.keys(mod)
.filter((n) => n !== "faker")
.map((fnName) => ({
name: ["faker", modName, fnName].join("."),
args: args(modName, fnName),
async onRender(_ctx, args) {
const fn = mod[fnName as keyof typeof mod] as (...a: unknown[]) => unknown;
const options = args.values.options;
// No options supplied
if (options == null || options === "") {
return normalizeResult(fn());
}
// Try JSON first
let parsed: unknown = options;
if (typeof options === "string") {
try {
parsed = JSON.parse(options);
} catch {
// Not valid JSON – maybe just a scalar
const n = Number(options);
if (!Number.isNaN(n)) {
parsed = n;
} else {
parsed = options;
}
}
}
let result: unknown;
if (Array.isArray(parsed)) {
// Treat as positional arguments
result = fn(...parsed);
} else {
// Treat as a single argument (option object or scalar)
result = fn(parsed);
}
return normalizeResult(result);
},
}));
}),
};

View File

@@ -0,0 +1,234 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`template-function-faker > exports all expected template functions 1`] = `
[
"faker.airline.aircraftType",
"faker.airline.airline",
"faker.airline.airplane",
"faker.airline.airport",
"faker.airline.flightNumber",
"faker.airline.recordLocator",
"faker.airline.seat",
"faker.animal.bear",
"faker.animal.bird",
"faker.animal.cat",
"faker.animal.cetacean",
"faker.animal.cow",
"faker.animal.crocodilia",
"faker.animal.dog",
"faker.animal.fish",
"faker.animal.horse",
"faker.animal.insect",
"faker.animal.lion",
"faker.animal.petName",
"faker.animal.rabbit",
"faker.animal.rodent",
"faker.animal.snake",
"faker.animal.type",
"faker.color.cmyk",
"faker.color.colorByCSSColorSpace",
"faker.color.cssSupportedFunction",
"faker.color.cssSupportedSpace",
"faker.color.hsl",
"faker.color.human",
"faker.color.hwb",
"faker.color.lab",
"faker.color.lch",
"faker.color.rgb",
"faker.color.space",
"faker.commerce.department",
"faker.commerce.isbn",
"faker.commerce.price",
"faker.commerce.product",
"faker.commerce.productAdjective",
"faker.commerce.productDescription",
"faker.commerce.productMaterial",
"faker.commerce.productName",
"faker.commerce.upc",
"faker.company.buzzAdjective",
"faker.company.buzzNoun",
"faker.company.buzzPhrase",
"faker.company.buzzVerb",
"faker.company.catchPhrase",
"faker.company.catchPhraseAdjective",
"faker.company.catchPhraseDescriptor",
"faker.company.catchPhraseNoun",
"faker.company.name",
"faker.database.collation",
"faker.database.column",
"faker.database.engine",
"faker.database.mongodbObjectId",
"faker.database.type",
"faker.date.anytime",
"faker.date.between",
"faker.date.betweens",
"faker.date.birthdate",
"faker.date.future",
"faker.date.month",
"faker.date.past",
"faker.date.recent",
"faker.date.soon",
"faker.date.timeZone",
"faker.date.weekday",
"faker.finance.accountName",
"faker.finance.accountNumber",
"faker.finance.amount",
"faker.finance.bic",
"faker.finance.bitcoinAddress",
"faker.finance.creditCardCVV",
"faker.finance.creditCardIssuer",
"faker.finance.creditCardNumber",
"faker.finance.currency",
"faker.finance.currencyCode",
"faker.finance.currencyName",
"faker.finance.currencyNumericCode",
"faker.finance.currencySymbol",
"faker.finance.ethereumAddress",
"faker.finance.iban",
"faker.finance.litecoinAddress",
"faker.finance.pin",
"faker.finance.routingNumber",
"faker.finance.transactionDescription",
"faker.finance.transactionType",
"faker.git.branch",
"faker.git.commitDate",
"faker.git.commitEntry",
"faker.git.commitMessage",
"faker.git.commitSha",
"faker.hacker.abbreviation",
"faker.hacker.adjective",
"faker.hacker.ingverb",
"faker.hacker.noun",
"faker.hacker.phrase",
"faker.hacker.verb",
"faker.image.avatar",
"faker.image.avatarGitHub",
"faker.image.dataUri",
"faker.image.personPortrait",
"faker.image.url",
"faker.image.urlLoremFlickr",
"faker.image.urlPicsumPhotos",
"faker.internet.displayName",
"faker.internet.domainName",
"faker.internet.domainSuffix",
"faker.internet.domainWord",
"faker.internet.email",
"faker.internet.emoji",
"faker.internet.exampleEmail",
"faker.internet.httpMethod",
"faker.internet.httpStatusCode",
"faker.internet.ip",
"faker.internet.ipv4",
"faker.internet.ipv6",
"faker.internet.jwt",
"faker.internet.jwtAlgorithm",
"faker.internet.mac",
"faker.internet.password",
"faker.internet.port",
"faker.internet.protocol",
"faker.internet.url",
"faker.internet.userAgent",
"faker.internet.username",
"faker.location.buildingNumber",
"faker.location.cardinalDirection",
"faker.location.city",
"faker.location.continent",
"faker.location.country",
"faker.location.countryCode",
"faker.location.county",
"faker.location.direction",
"faker.location.language",
"faker.location.latitude",
"faker.location.longitude",
"faker.location.nearbyGPSCoordinate",
"faker.location.ordinalDirection",
"faker.location.postalAddress",
"faker.location.secondaryAddress",
"faker.location.state",
"faker.location.street",
"faker.location.streetAddress",
"faker.location.timeZone",
"faker.location.zipCode",
"faker.lorem.lines",
"faker.lorem.paragraph",
"faker.lorem.paragraphs",
"faker.lorem.sentence",
"faker.lorem.sentences",
"faker.lorem.slug",
"faker.lorem.text",
"faker.lorem.word",
"faker.lorem.words",
"faker.music.album",
"faker.music.artist",
"faker.music.genre",
"faker.music.songName",
"faker.number.bigInt",
"faker.number.binary",
"faker.number.float",
"faker.number.hex",
"faker.number.int",
"faker.number.octal",
"faker.number.romanNumeral",
"faker.person.bio",
"faker.person.firstName",
"faker.person.fullName",
"faker.person.gender",
"faker.person.jobArea",
"faker.person.jobDescriptor",
"faker.person.jobTitle",
"faker.person.jobType",
"faker.person.lastName",
"faker.person.middleName",
"faker.person.prefix",
"faker.person.sex",
"faker.person.sexType",
"faker.person.suffix",
"faker.person.zodiacSign",
"faker.phone.imei",
"faker.phone.number",
"faker.science.chemicalElement",
"faker.science.unit",
"faker.string.alpha",
"faker.string.alphanumeric",
"faker.string.binary",
"faker.string.fromCharacters",
"faker.string.hexadecimal",
"faker.string.nanoid",
"faker.string.numeric",
"faker.string.octal",
"faker.string.sample",
"faker.string.symbol",
"faker.string.ulid",
"faker.string.uuid",
"faker.system.commonFileExt",
"faker.system.commonFileName",
"faker.system.commonFileType",
"faker.system.cron",
"faker.system.directoryPath",
"faker.system.fileExt",
"faker.system.fileName",
"faker.system.filePath",
"faker.system.fileType",
"faker.system.mimeType",
"faker.system.networkInterface",
"faker.system.semver",
"faker.vehicle.bicycle",
"faker.vehicle.color",
"faker.vehicle.fuel",
"faker.vehicle.manufacturer",
"faker.vehicle.model",
"faker.vehicle.type",
"faker.vehicle.vehicle",
"faker.vehicle.vin",
"faker.vehicle.vrm",
"faker.word.adjective",
"faker.word.adverb",
"faker.word.conjunction",
"faker.word.interjection",
"faker.word.noun",
"faker.word.preposition",
"faker.word.sample",
"faker.word.verb",
"faker.word.words",
]
`;

View File

@@ -0,0 +1,31 @@
import { describe, expect, it } from "vite-plus/test";
describe("template-function-faker", () => {
it("exports all expected template functions", async () => {
const { plugin } = await import("../src/index");
const names = plugin.templateFunctions?.map((fn) => fn.name).sort() ?? [];
// Snapshot the full list of exported function names so we catch any
// accidental additions, removals, or renames across faker upgrades.
expect(names).toMatchSnapshot();
});
it("renders date results as unquoted ISO strings", async () => {
const { plugin } = await import("../src/index");
const fn = plugin.templateFunctions?.find((fn) => fn.name === "faker.date.future");
// oxlint-disable-next-line unbound-method
const onRender = fn?.onRender;
expect(onRender).toBeTypeOf("function");
if (onRender == null) {
throw new Error("Expected template function 'faker.date.future' to define onRender");
}
const result = await onRender(
{} as Parameters<typeof onRender>[0],
{ values: {} } as Parameters<typeof onRender>[1],
);
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
});
});

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -0,0 +1,52 @@
# Yaak HTTP Snippet Plugin
Generate code snippets for HTTP requests in various languages and frameworks,
powered by [@readme/httpsnippet](https://github.com/readmeio/httpsnippet).
![Httpsnippet plugin](https://assets.yaak.app/uploads/httpsnippet-guiaX_1786x1420.png)
## How It Works
Right-click any HTTP request (or use the `...` menu) and select **Generate Code Snippet**.
A dialog lets you pick a language and library, with a live preview of the generated code.
Click **Copy to Clipboard** to copy the snippet. Your language and library selections are
remembered for next time.
## Supported Languages
Each language supports one or more libraries, listed in the order they appear in the
dialog:
| Language | Libraries |
| ----------- | ----------------------------------------- |
| Agent | Agent Prompt |
| C | Libcurl |
| Clojure | clj-http |
| Crystal | http::client |
| C# | HttpClient, RestSharp |
| Go | NewRequest |
| HTTP | HTTP/1.1 |
| Java | AsyncHttp, java.net.http, OkHttp, Unirest |
| JavaScript | XMLHttpRequest, Axios, fetch, jQuery |
| Kotlin | OkHttp |
| Node.js | HTTP, Axios, fetch |
| Objective-C | NSURLSession |
| OCaml | CoHTTP |
| PHP | cURL, Guzzle, HTTP v1, HTTP v2 |
| Powershell | Invoke-WebRequest, Invoke-RestMethod |
| Python | Requests |
| R | httr |
| Ruby | net::http, faraday |
| Rust | reqwest |
| Shell | cURL, HTTPie, Wget |
| Swift | URLSession |
The **Agent** target produces a prompt describing the request rather than code, for handing
to an AI coding agent.
## Features
- Renders template variables before generating snippets, so the output reflects real values
- Supports all body types: JSON, form-urlencoded, multipart, GraphQL, and raw text
- Includes authentication headers (Basic, Bearer, and API Key)
- Includes query parameters and custom headers

View File

@@ -0,0 +1,24 @@
{
"name": "@yaak/httpsnippet",
"displayName": "HTTP Snippet",
"version": "1.0.3",
"private": true,
"description": "Generate code snippets for HTTP requests in various languages and frameworks",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins-external/httpsnippet"
},
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev"
},
"dependencies": {
"@readme/httpsnippet": "^11.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.9.3"
},
"minYaakVersion": "2026.2.0-beta.10"
}

View File

@@ -0,0 +1,317 @@
import { availableTargets, type HarRequest, HTTPSnippet } from "@readme/httpsnippet";
import type { EditorLanguage, HttpRequest, PluginDefinition } from "@yaakapp/api";
// Get all available targets and build select options
const targets = availableTargets();
// Targets to exclude from the language list
const excludedTargets = new Set(["json"]);
// Build language (target) options
const languageOptions = targets
.filter((target) => !excludedTargets.has(target.key))
.map((target) => ({
label: target.title,
value: target.key,
}));
// Preferred clients per target (shown first in the list)
const preferredClients: Record<string, string> = {
javascript: "fetch",
node: "fetch",
};
// Get client options for a given target key
function getClientOptions(targetKey: string) {
const target = targets.find((t) => t.key === targetKey);
if (!target) return [];
const preferred = preferredClients[targetKey];
return target.clients
.map((client) => ({
label: client.title,
value: client.key,
}))
.sort((a, b) => {
if (a.value === preferred) return -1;
if (b.value === preferred) return 1;
return 0;
});
}
// Get default client for a target
function getDefaultClient(targetKey: string): string {
const options = getClientOptions(targetKey);
return options[0]?.value ?? "";
}
// Defaults
const defaultTarget = "javascript";
// Map httpsnippet target key to editor language for syntax highlighting
const editorLanguageMap: Record<string, EditorLanguage> = {
c: "c",
clojure: "clojure",
csharp: "csharp",
go: "go",
http: "http",
java: "java",
javascript: "javascript",
kotlin: "kotlin",
node: "javascript",
objc: "objective_c",
ocaml: "ocaml",
php: "php",
powershell: "powershell",
python: "python",
r: "r",
ruby: "ruby",
shell: "shell",
swift: "swift",
};
function getEditorLanguage(targetKey: string): EditorLanguage {
return editorLanguageMap[targetKey] ?? "text";
}
// Convert Yaak HttpRequest to HAR format
function toHarRequest(request: Partial<HttpRequest>) {
// Build URL with query parameters
let finalUrl = request.url || "";
const urlParams = (request.urlParameters ?? []).filter((p) => p.enabled !== false && !!p.name);
if (urlParams.length > 0) {
const [base, hash] = finalUrl.split("#");
const separator = base?.includes("?") ? "&" : "?";
const queryString = urlParams
.map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`)
.join("&");
finalUrl = base + separator + queryString + (hash ? `#${hash}` : "");
}
// Build headers array
const headers: Array<{ name: string; value: string }> = (request.headers ?? [])
.filter((h) => h.enabled !== false && !!h.name)
.map((h) => ({ name: h.name, value: h.value }));
// Handle authentication
if (request.authentication?.disabled !== true) {
if (request.authenticationType === "basic") {
const credentials = btoa(
`${request.authentication?.username ?? ""}:${request.authentication?.password ?? ""}`,
);
headers.push({ name: "Authorization", value: `Basic ${credentials}` });
} else if (request.authenticationType === "bearer") {
const prefix = request.authentication?.prefix ?? "Bearer";
const token = request.authentication?.token ?? "";
headers.push({ name: "Authorization", value: `${prefix} ${token}`.trim() });
} else if (request.authenticationType === "apikey") {
if (request.authentication?.location === "header") {
headers.push({
name: request.authentication?.key ?? "X-Api-Key",
value: request.authentication?.value ?? "",
});
} else if (request.authentication?.location === "query") {
const sep = finalUrl.includes("?") ? "&" : "?";
finalUrl = [
finalUrl,
sep,
encodeURIComponent(request.authentication?.key ?? "token"),
"=",
encodeURIComponent(request.authentication?.value ?? ""),
].join("");
}
}
}
// Build HAR request object
const har: Record<string, unknown> = {
method: request.method || "GET",
url: finalUrl,
headers,
};
// Handle request body
const bodyType = request.bodyType ?? "none";
if (bodyType !== "none" && request.body) {
if (bodyType === "application/x-www-form-urlencoded" && Array.isArray(request.body.form)) {
const params = request.body.form
.filter((p: { enabled?: boolean; name?: string }) => p.enabled !== false && !!p.name)
.map((p: { name: string; value: string }) => ({ name: p.name, value: p.value }));
har.postData = {
mimeType: "application/x-www-form-urlencoded",
params,
};
} else if (bodyType === "multipart/form-data" && Array.isArray(request.body.form)) {
const params = request.body.form
.filter((p: { enabled?: boolean; name?: string }) => p.enabled !== false && !!p.name)
.map((p: { name: string; value: string; file?: string; contentType?: string }) => {
const param: Record<string, string> = { name: p.name, value: p.value || "" };
if (p.file) param.fileName = p.file;
if (p.contentType) param.contentType = p.contentType;
return param;
});
har.postData = {
mimeType: "multipart/form-data",
params,
};
} else if (bodyType === "graphql" && typeof request.body.query === "string") {
const body = {
query: request.body.query || "",
variables: maybeParseJSON(request.body.variables, undefined),
};
har.postData = {
mimeType: "application/json",
text: JSON.stringify(body),
};
} else if (typeof request.body.text === "string") {
har.postData = {
mimeType: bodyType,
text: request.body.text,
};
}
}
return har;
}
function maybeParseJSON<T>(v: unknown, fallback: T): unknown {
if (typeof v !== "string") return fallback;
try {
return JSON.parse(v);
} catch {
return fallback;
}
}
export const plugin: PluginDefinition = {
httpRequestActions: [
{
label: "Generate Code Snippet",
icon: "copy",
async onSelect(ctx, args) {
// Render the request with variables resolved
const renderedRequest = await ctx.httpRequest.render({
httpRequest: args.httpRequest,
purpose: "send",
});
// Convert to HAR format
const harRequest = toHarRequest(renderedRequest) as HarRequest;
// Get previously selected language or use defaults
const storedTarget = await ctx.store.get<string>("selectedTarget");
const initialTarget = storedTarget || defaultTarget;
const storedClient = await ctx.store.get<string>(`selectedClient:${initialTarget}`);
const initialClient = storedClient || getDefaultClient(initialTarget);
// Create snippet generator
const snippet = new HTTPSnippet(harRequest);
const generateSnippet = (target: string, client: string): string => {
const result = snippet.convert(
target as Parameters<typeof snippet.convert>[0],
client as Parameters<typeof snippet.convert>[1],
);
return (Array.isArray(result) ? result.join("\n") : result || "").replace(/\r\n/g, "\n");
};
// Generate initial code preview
let initialCode = "";
try {
initialCode = generateSnippet(initialTarget, initialClient);
} catch {
initialCode = "// Error generating snippet";
}
// Show dialog with language/library selectors and code preview
const result = await ctx.prompt.form({
id: "httpsnippet",
title: "Generate Code Snippet",
confirmText: "Copy to Clipboard",
cancelText: "Cancel",
size: "md",
inputs: [
{
type: "h_stack",
inputs: [
{
type: "select",
name: "target",
label: "Language",
defaultValue: initialTarget,
options: languageOptions,
},
{
type: "select",
name: `client-${initialTarget}`,
label: "Library",
defaultValue: initialClient,
options: getClientOptions(initialTarget),
dynamic(_ctx, { values }) {
const targetKey = String(values.target || defaultTarget);
const options = getClientOptions(targetKey);
return {
name: `client-${targetKey}`,
options,
defaultValue: options[0]?.value ?? "",
};
},
},
],
},
{
type: "editor",
name: "code",
label: "Preview",
language: getEditorLanguage(initialTarget),
defaultValue: initialCode,
readOnly: true,
rows: 15,
dynamic(_ctx, { values }) {
const targetKey = String(values.target || defaultTarget);
const clientKey = String(
values[`client-${targetKey}`] || getDefaultClient(targetKey),
);
let code: string;
try {
code = generateSnippet(targetKey, clientKey);
} catch {
code = "// Error generating snippet";
}
return {
defaultValue: code,
language: getEditorLanguage(targetKey),
};
},
},
],
});
if (result) {
// Store the selected language and library for next time
const selectedTarget = String(result.target || initialTarget);
const selectedClient = String(
result[`client-${selectedTarget}`] || getDefaultClient(selectedTarget),
);
await ctx.store.set("selectedTarget", selectedTarget);
await ctx.store.set(`selectedClient:${selectedTarget}`, selectedClient);
// Generate snippet for the selected language
try {
const codeText = generateSnippet(selectedTarget, selectedClient);
await ctx.clipboard.copyText(codeText);
await ctx.toast.show({
message: "Code snippet copied to clipboard",
icon: "copy",
color: "success",
});
} catch (err) {
await ctx.toast.show({
message: `Failed to generate snippet: ${err instanceof Error ? err.message : String(err)}`,
icon: "alert_triangle",
color: "danger",
});
}
}
},
},
],
};

View File

@@ -0,0 +1,58 @@
# Yaak MCP Server Plugin
Exposes Yaak's functionality via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/).
## Setup
Add this to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
```json
{
"mcpServers": {
"yaak": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://127.0.0.1:64343/mcp"]
}
}
}
```
Restart Claude Desktop and make sure Yaak is running.
## Available Tools
- `list_http_requests` - List all HTTP requests in a workspace
- `get_http_request` - Get details of a specific HTTP request
- `send_http_request` - Send an HTTP request and get the response
- `create_http_request` - Create a new HTTP request
- `update_http_request` - Update an existing HTTP request
- `delete_http_request` - Delete an HTTP request
- `list_folders` - List all folders in a workspace
- `list_workspaces` - List all open workspaces
- `get_workspace_id` - Get the current workspace ID
- `get_environment_id` - Get the current environment ID
- `copy_to_clipboard` - Copy text to the system clipboard
- `show_toast` - Show a toast notification in Yaak
## Environment Selection
`send_http_request` accepts an optional `environmentId` for that execution. It overrides the
active environment without changing the selection in Yaak. Omitting it uses the host's
active environment, as before.
The ID must identify a base or sub-environment in the request's workspace. An empty,
unknown, foreign-workspace, or folder environment ID returns an error before sending.
Use the base environment ID to run with global variables only. Folder variables still
apply according to the request's folder hierarchy.
```json
{
"id": "rq_example",
"workspaceId": "wk_example",
"environmentId": "ev_staging"
}
```
This requires a Yaak host with support for the `environmentId` plugin API field;
older hosts ignore it. `get_environment_id` reports the active selection, not the
override used by a previous send.

View File

@@ -0,0 +1,28 @@
{
"name": "@yaak/mcp-server",
"displayName": "MCP Server",
"version": "0.2.1",
"private": true,
"description": "Expose Yaak functionality via Model Context Protocol",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins-external/mcp-server"
},
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev"
},
"dependencies": {
"@hono/mcp": "^0.3.2",
"@hono/node-server": "^2.0.10",
"@modelcontextprotocol/sdk": "^1.26.0",
"hono": "^4.13.5",
"zod": "^4.6.2"
},
"devDependencies": {
"@types/node": "^25.0.3",
"typescript": "^5.9.3"
},
"minYaakVersion": "2026.1.0"
}

View File

@@ -0,0 +1,36 @@
import type { Context, PluginDefinition } from "@yaakapp/api";
import { createMcpServer } from "./server.js";
const serverPort = parseInt(process.env.YAAK_PLUGIN_MCP_SERVER_PORT ?? "64343", 10);
let mcpServer: Awaited<ReturnType<typeof createMcpServer>> | null = null;
export const plugin: PluginDefinition = {
async init(ctx: Context) {
// Start the server after waiting, so there's an active window open to do things
// like show the startup toast.
console.log("Initializing MCP Server plugin");
setTimeout(async () => {
try {
mcpServer = createMcpServer({ yaak: ctx }, serverPort);
} catch (err) {
console.error("Failed to start MCP server:", err);
void ctx.toast.show({
message: `Failed to start MCP Server: ${err instanceof Error ? err.message : String(err)}`,
icon: "alert_triangle",
color: "danger",
timeout: 10000,
});
}
}, 5000);
},
async dispose() {
console.log("Disposing MCP Server plugin");
if (mcpServer) {
await mcpServer.close();
mcpServer = null;
}
},
};

View File

@@ -0,0 +1,72 @@
import { StreamableHTTPTransport } from "@hono/mcp";
import { serve } from "@hono/node-server";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Hono } from "hono";
import { registerFolderTools } from "./tools/folder.js";
import { registerHttpRequestTools } from "./tools/httpRequest.js";
import { registerToastTools } from "./tools/toast.js";
import { registerWindowTools } from "./tools/window.js";
import { registerWorkspaceTools } from "./tools/workspace.js";
import type { McpServerContext } from "./types.js";
export function createMcpServer(ctx: McpServerContext, port: number) {
console.log("Creating MCP server on port", port);
const mcpServer = new McpServer({
name: "yaak-mcp-server",
version: "0.1.0",
});
// Register all tools
registerToastTools(mcpServer, ctx);
registerHttpRequestTools(mcpServer, ctx);
registerFolderTools(mcpServer, ctx);
registerWindowTools(mcpServer, ctx);
registerWorkspaceTools(mcpServer, ctx);
const app = new Hono();
const transport = new StreamableHTTPTransport();
app.all("/mcp", async (c) => {
if (!mcpServer.isConnected()) {
// Connect the mcp with the transport
await mcpServer.connect(transport);
void ctx.yaak.toast.show({
message: `MCP Server connected`,
icon: "info",
color: "info",
timeout: 5000,
});
}
return transport.handleRequest(c);
});
const honoServer = serve(
{
port,
hostname: "127.0.0.1",
fetch: app.fetch,
},
(info) => {
console.log("Started MCP server on ", info.address);
void ctx.yaak.toast.show({
message: `MCP Server running on http://127.0.0.1:${info.port}`,
icon: "info",
color: "secondary",
timeout: 10000,
});
},
);
return {
server: mcpServer,
close: async () => {
await new Promise<void>((resolve, reject) => {
honoServer.close((err) => {
if (err) reject(err);
else resolve();
});
});
await mcpServer.close();
},
};
}

View File

@@ -0,0 +1,148 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import * as z from "zod";
import type { McpServerContext } from "../types.js";
import { getWorkspaceContext } from "./helpers.js";
import {
authenticationSchema,
authenticationTypeSchema,
headersSchema,
workspaceIdSchema,
} from "./schemas.js";
export function registerFolderTools(server: McpServer, ctx: McpServerContext) {
server.registerTool(
"list_folders",
{
title: "List Folders",
description: "List all folders in a workspace",
inputSchema: {
workspaceId: workspaceIdSchema,
},
},
async ({ workspaceId }) => {
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
const folders = await workspaceCtx.yaak.folder.list();
return {
content: [
{
type: "text" as const,
text: JSON.stringify(folders, null, 2),
},
],
};
},
);
server.registerTool(
"get_folder",
{
title: "Get Folder",
description: "Get details of a specific folder by ID",
inputSchema: {
id: z.string().describe("The folder ID"),
workspaceId: workspaceIdSchema,
},
},
async ({ id, workspaceId }) => {
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
const folder = await workspaceCtx.yaak.folder.getById({ id });
return {
content: [
{
type: "text" as const,
text: JSON.stringify(folder, null, 2),
},
],
};
},
);
server.registerTool(
"create_folder",
{
title: "Create Folder",
description: "Create a new folder in a workspace",
inputSchema: {
workspaceId: workspaceIdSchema,
name: z.string().describe("Folder name"),
folderId: z.string().optional().describe("Parent folder ID (for nested folders)"),
description: z.string().optional().describe("Folder description"),
sortPriority: z.number().optional().describe("Sort priority for ordering"),
headers: headersSchema.describe("Default headers to apply to requests in this folder"),
authenticationType: authenticationTypeSchema,
authentication: authenticationSchema,
},
},
async ({ workspaceId: ogWorkspaceId, ...args }) => {
const workspaceCtx = await getWorkspaceContext(ctx, ogWorkspaceId);
const workspaceId = await workspaceCtx.yaak.window.workspaceId();
if (!workspaceId) {
throw new Error("No workspace is open");
}
const folder = await workspaceCtx.yaak.folder.create({
workspaceId: workspaceId,
...args,
});
return {
content: [{ type: "text" as const, text: JSON.stringify(folder, null, 2) }],
};
},
);
server.registerTool(
"update_folder",
{
title: "Update Folder",
description: "Update an existing folder",
inputSchema: {
id: z.string().describe("Folder ID to update"),
workspaceId: workspaceIdSchema,
name: z.string().optional().describe("Folder name"),
folderId: z.string().optional().describe("Parent folder ID (for nested folders)"),
description: z.string().optional().describe("Folder description"),
sortPriority: z.number().optional().describe("Sort priority for ordering"),
headers: headersSchema.describe("Default headers to apply to requests in this folder"),
authenticationType: authenticationTypeSchema,
authentication: authenticationSchema,
},
},
async ({ id, workspaceId, ...updates }) => {
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
// Fetch existing folder to merge with updates
const existing = await workspaceCtx.yaak.folder.getById({ id });
if (!existing) {
throw new Error(`Folder with ID ${id} not found`);
}
// Merge existing fields with updates
const folder = await workspaceCtx.yaak.folder.update({
...existing,
...updates,
id,
});
return {
content: [{ type: "text" as const, text: JSON.stringify(folder, null, 2) }],
};
},
);
server.registerTool(
"delete_folder",
{
title: "Delete Folder",
description: "Delete a folder by ID",
inputSchema: {
id: z.string().describe("Folder ID to delete"),
},
},
async ({ id }) => {
const folder = await ctx.yaak.folder.delete({ id });
return {
content: [{ type: "text" as const, text: `Deleted: ${folder.name} (${folder.id})` }],
};
},
);
}

View File

@@ -0,0 +1,32 @@
import type { McpServerContext } from "../types.js";
export async function getWorkspaceContext(
ctx: McpServerContext,
workspaceId?: string,
): Promise<McpServerContext> {
const workspaces = await ctx.yaak.workspace.list();
if (!workspaceId && workspaces.length > 1) {
const workspaceList = workspaces.map((w, i) => `${i + 1}. ${w.name} (ID: ${w.id})`).join("\n");
throw new Error(
`Multiple workspaces are open. Please specify which workspace to use.\n\n` +
`Currently open workspaces:\n${workspaceList}\n\n` +
`You can use the list_workspaces tool to get workspace IDs, then use other tools ` +
`with the workspace context. For example, ask the user which workspace they want ` +
`to work with by presenting them with the numbered list above.`,
);
}
const workspace = workspaceId ? workspaces.find((w) => w.id === workspaceId) : workspaces[0];
if (!workspace) {
const workspaceList = workspaces.map((w) => `- ${w.name} (ID: ${w.id})`).join("\n");
throw new Error(
`Workspace with ID "${workspaceId}" not found.\n\n` +
`Available workspaces:\n${workspaceList}`,
);
}
return {
yaak: ctx.yaak.workspace.withContext(workspace),
};
}

View File

@@ -0,0 +1,98 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { Context } from "@yaakapp/api";
import { afterEach, expect, test, vi } from "vite-plus/test";
import { registerHttpRequestTools } from "./httpRequest";
const cleanups: Array<() => Promise<void>> = [];
afterEach(async () => {
for (const cleanup of cleanups.splice(0)) await cleanup();
});
async function fixture({ missing = false, sendError = false } = {}) {
const httpRequest = { id: "rq_test", workspaceId: "wk_test", url: "http://localhost/echo" };
const send = vi.fn(async (_args: { httpRequest: unknown; environmentId?: string }) => {
if (sendError) throw new Error("Environment not found");
return { httpResponse: { id: "rs_test", status: 200 } };
});
// Only the host is mocked; calls go through the real MCP client, schema and transport.
const yaak = {
workspace: {
list: async () => [{ id: "wk_test", name: "Test Workspace" }],
withContext: () => yaak,
},
httpRequest: { getById: async () => (missing ? null : httpRequest), send },
};
const server = new McpServer({ name: "yaak-test", version: "0.0.0" });
registerHttpRequestTools(server, { yaak: yaak as unknown as Context });
const client = new Client({ name: "test", version: "0.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
cleanups.push(async () => {
await client.close();
await server.close();
});
return { client, send, httpRequest };
}
test("forwards the advertised environmentId to the host", async () => {
const { client, send, httpRequest } = await fixture();
const { tools } = await client.listTools();
expect(
tools.find((tool) => tool.name === "send_http_request")?.inputSchema.properties,
).toHaveProperty("environmentId");
const result = await client.callTool({
name: "send_http_request",
arguments: { id: httpRequest.id, workspaceId: "wk_test", environmentId: "ev_staging" },
});
expect(result.isError).toBeFalsy();
expect(send).toHaveBeenCalledExactlyOnceWith({ httpRequest, environmentId: "ev_staging" });
});
test("leaves the environment unspecified when omitted", async () => {
const { client, send, httpRequest } = await fixture();
const result = await client.callTool({
name: "send_http_request",
arguments: { id: httpRequest.id },
});
expect(result.isError).toBeFalsy();
expect(send).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ httpRequest }));
expect(send.mock.calls[0]?.[0]).not.toHaveProperty("environmentId", expect.any(String));
});
test("does not send a missing request", async () => {
const { client, send } = await fixture({ missing: true });
const result = await client.callTool({
name: "send_http_request",
arguments: { id: "rq_missing" },
});
expect(result.isError).toBe(true);
expect(send).not.toHaveBeenCalled();
});
test("returns host environment errors to the MCP client", async () => {
const { client } = await fixture({ sendError: true });
const result = await client.callTool({
name: "send_http_request",
arguments: { id: "rq_test", environmentId: "ev_missing" },
});
expect(result.isError).toBe(true);
expect(result.content).toEqual([{ type: "text", text: "Environment not found" }]);
});
test("keeps concurrent environment overrides separate", async () => {
const { client, send, httpRequest } = await fixture();
await Promise.all(
["ev_a", "ev_b"].map((environmentId) =>
client.callTool({
name: "send_http_request",
arguments: { id: httpRequest.id, environmentId },
}),
),
);
expect(send).toHaveBeenCalledTimes(2);
expect(send).toHaveBeenCalledWith({ httpRequest, environmentId: "ev_a" });
expect(send).toHaveBeenCalledWith({ httpRequest, environmentId: "ev_b" });
});

View File

@@ -0,0 +1,201 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import * as z from "zod";
import type { McpServerContext } from "../types.js";
import { getWorkspaceContext } from "./helpers.js";
import {
authenticationSchema,
authenticationTypeSchema,
bodySchema,
bodyTypeSchema,
headersSchema,
urlParametersSchema,
workspaceIdSchema,
} from "./schemas.js";
export function registerHttpRequestTools(server: McpServer, ctx: McpServerContext) {
server.registerTool(
"list_http_requests",
{
title: "List HTTP Requests",
description: "List all HTTP requests in a workspace",
inputSchema: {
workspaceId: workspaceIdSchema,
},
},
async ({ workspaceId }) => {
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
const requests = await workspaceCtx.yaak.httpRequest.list();
return {
content: [
{
type: "text" as const,
text: JSON.stringify(requests, null, 2),
},
],
};
},
);
server.registerTool(
"get_http_request",
{
title: "Get HTTP Request",
description: "Get details of a specific HTTP request by ID",
inputSchema: {
id: z.string().describe("The HTTP request ID"),
workspaceId: workspaceIdSchema,
},
},
async ({ id, workspaceId }) => {
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
const request = await workspaceCtx.yaak.httpRequest.getById({ id });
return {
content: [
{
type: "text" as const,
text: JSON.stringify(request, null, 2),
},
],
};
},
);
server.registerTool(
"send_http_request",
{
title: "Send HTTP Request",
description: "Send an HTTP request and get the response",
inputSchema: {
id: z.string().describe("The HTTP request ID to send"),
environmentId: z
.string()
.optional()
.describe("Environment ID for this send only; defaults to the active environment"),
workspaceId: workspaceIdSchema,
},
},
async ({ id, workspaceId, environmentId }) => {
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
const httpRequest = await workspaceCtx.yaak.httpRequest.getById({ id });
if (httpRequest == null) {
throw new Error(`HTTP request with ID ${id} not found`);
}
const { httpResponse: response } = await workspaceCtx.yaak.httpRequest.send({
httpRequest,
environmentId,
});
return {
content: [
{
type: "text" as const,
text: JSON.stringify(response, null, 2),
},
],
};
},
);
server.registerTool(
"create_http_request",
{
title: "Create HTTP Request",
description: "Create a new HTTP request",
inputSchema: {
workspaceId: workspaceIdSchema,
name: z
.string()
.optional()
.describe("Request name (empty string to auto-generate from URL)"),
url: z.string().describe("Request URL"),
method: z.string().optional().describe("HTTP method (defaults to GET)"),
folderId: z.string().optional().describe("Parent folder ID"),
description: z.string().optional().describe("Request description"),
headers: headersSchema.describe("Request headers"),
urlParameters: urlParametersSchema,
bodyType: bodyTypeSchema,
body: bodySchema,
authenticationType: authenticationTypeSchema,
authentication: authenticationSchema,
},
},
async ({ workspaceId: ogWorkspaceId, ...args }) => {
const workspaceCtx = await getWorkspaceContext(ctx, ogWorkspaceId);
const workspaceId = await workspaceCtx.yaak.window.workspaceId();
if (!workspaceId) {
throw new Error("No workspace is open");
}
const httpRequest = await workspaceCtx.yaak.httpRequest.create({
workspaceId: workspaceId,
...args,
});
return {
content: [{ type: "text" as const, text: JSON.stringify(httpRequest, null, 2) }],
};
},
);
server.registerTool(
"update_http_request",
{
title: "Update HTTP Request",
description: "Update an existing HTTP request",
inputSchema: {
id: z.string().describe("HTTP request ID to update"),
workspaceId: workspaceIdSchema,
name: z.string().optional().describe("Request name"),
url: z.string().optional().describe("Request URL"),
method: z.string().optional().describe("HTTP method"),
folderId: z.string().optional().describe("Parent folder ID"),
description: z.string().optional().describe("Request description"),
headers: headersSchema.describe("Request headers"),
urlParameters: urlParametersSchema,
bodyType: bodyTypeSchema,
body: bodySchema,
authenticationType: authenticationTypeSchema,
authentication: authenticationSchema,
},
},
async ({ id, workspaceId, ...updates }) => {
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
// Fetch existing request to merge with updates
const existing = await workspaceCtx.yaak.httpRequest.getById({ id });
if (!existing) {
throw new Error(`HTTP request with ID ${id} not found`);
}
// Merge existing fields with updates
const httpRequest = await workspaceCtx.yaak.httpRequest.update({
...existing,
...updates,
id,
});
return {
content: [{ type: "text" as const, text: JSON.stringify(httpRequest, null, 2) }],
};
},
);
server.registerTool(
"delete_http_request",
{
title: "Delete HTTP Request",
description: "Delete an HTTP request by ID",
inputSchema: {
id: z.string().describe("HTTP request ID to delete"),
},
},
async ({ id }) => {
const httpRequest = await ctx.yaak.httpRequest.delete({ id });
return {
content: [
{ type: "text" as const, text: `Deleted: ${httpRequest.name} (${httpRequest.id})` },
],
};
},
);
}

View File

@@ -0,0 +1,67 @@
import * as z from "zod";
export const workspaceIdSchema = z
.string()
.optional()
.describe("Workspace ID (required if multiple workspaces are open)");
export const headersSchema = z
.array(
z.object({
name: z.string(),
value: z.string(),
enabled: z.boolean().default(true),
}),
)
.optional();
export const urlParametersSchema = z
.array(
z.object({
name: z.string(),
value: z.string(),
enabled: z.boolean().default(true),
}),
)
.optional()
.describe("URL query parameters");
export const bodyTypeSchema = z
.string()
.optional()
.describe(
'Body type. Supported values: "binary", "graphql", "application/x-www-form-urlencoded", "multipart/form-data", or any text-based type (e.g., "application/json", "text/plain")',
);
export const bodySchema = z
.record(z.string(), z.any())
.optional()
.describe(
"Body content object. Structure varies by bodyType:\n" +
'- "binary": { filePath: "/path/to/file" }\n' +
'- "graphql": { query: "{ users { name } }", variables: "{\\"id\\": \\"123\\"}" }\n' +
'- "application/x-www-form-urlencoded": { form: [{ name: "key", value: "val", enabled: true }] }\n' +
'- "multipart/form-data": { form: [{ name: "field", value: "text", file: "/path/to/file", enabled: true }] }\n' +
'- text-based (application/json, etc.): { text: "raw body content" }',
);
export const authenticationTypeSchema = z
.string()
.optional()
.describe(
'Authentication type. Common values: "basic", "bearer", "oauth2", "apikey", "jwt", "awsv4", "oauth1", "ntlm", "none". Use null to inherit from parent.',
);
export const authenticationSchema = z
.record(z.string(), z.any())
.optional()
.describe(
"Authentication configuration object. Structure varies by authenticationType:\n" +
'- "basic": { username: "user", password: "pass" }\n' +
'- "bearer": { token: "abc123", prefix: "Bearer" }\n' +
'- "oauth2": { clientId: "...", clientSecret: "...", grantType: "authorization_code", authorizationUrl: "...", accessTokenUrl: "...", scope: "...", ... }\n' +
'- "apikey": { location: "header" | "query", key: "X-API-Key", value: "..." }\n' +
'- "jwt": { algorithm: "HS256", secret: "...", payload: "{ ... }" }\n' +
'- "awsv4": { accessKeyId: "...", secretAccessKey: "...", service: "sts", region: "us-east-1", sessionToken: "..." }\n' +
'- "none": {}',
);

View File

@@ -0,0 +1,59 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { Color, Icon } from "@yaakapp/api";
import * as z from "zod";
import type { McpServerContext } from "../types.js";
const ICON_VALUES = [
"alert_triangle",
"check",
"check_circle",
"chevron_down",
"copy",
"info",
"pin",
"search",
"trash",
] as const satisfies readonly Icon[];
const COLOR_VALUES = [
"primary",
"secondary",
"info",
"success",
"notice",
"warning",
"danger",
] as const satisfies readonly Color[];
export function registerToastTools(server: McpServer, ctx: McpServerContext) {
server.registerTool(
"show_toast",
{
title: "Show Toast",
description: "Show a toast notification in Yaak",
inputSchema: {
message: z.string().describe("The message to display"),
icon: z.enum(ICON_VALUES).optional().describe("Icon name"),
color: z.enum(COLOR_VALUES).optional().describe("Toast color"),
timeout: z.number().optional().describe("Timeout in milliseconds"),
},
},
async ({ message, icon, color, timeout }) => {
await ctx.yaak.toast.show({
message,
icon,
color,
timeout,
});
return {
content: [
{
type: "text" as const,
text: `✓ Toast shown: "${message}"`,
},
],
};
},
);
}

View File

@@ -0,0 +1,47 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { McpServerContext } from "../types.js";
import { getWorkspaceContext } from "./helpers.js";
export function registerWindowTools(server: McpServer, ctx: McpServerContext) {
server.registerTool(
"get_workspace_id",
{
title: "Get Workspace ID",
description: "Get the current workspace ID",
},
async () => {
const workspaceCtx = await getWorkspaceContext(ctx);
const workspaceId = await workspaceCtx.yaak.window.workspaceId();
return {
content: [
{
type: "text" as const,
text: workspaceId || "No workspace open",
},
],
};
},
);
server.registerTool(
"get_environment_id",
{
title: "Get Environment ID",
description: "Get the current environment ID",
},
async () => {
const workspaceCtx = await getWorkspaceContext(ctx);
const environmentId = await workspaceCtx.yaak.window.environmentId();
return {
content: [
{
type: "text" as const,
text: environmentId || "No environment selected",
},
],
};
},
);
}

View File

@@ -0,0 +1,24 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { McpServerContext } from "../types.js";
export function registerWorkspaceTools(server: McpServer, ctx: McpServerContext) {
server.registerTool(
"list_workspaces",
{
title: "List Workspaces",
description: "List all open workspaces in Yaak",
},
async () => {
const workspaces = await ctx.yaak.workspace.list();
return {
content: [
{
type: "text" as const,
text: JSON.stringify(workspaces, null, 2),
},
],
};
},
);
}

View File

@@ -0,0 +1,5 @@
import type { Context } from "@yaakapp/api";
export interface McpServerContext {
yaak: Context;
}

View File

@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"skipLibCheck": true,
"moduleResolution": "Bundler"
}
}