snapshot of https://github.com/mountain-loop/yaak.git (main, 2026-09-23; blobs > 131072B omitted)
This commit is contained in:
1
plugins-external/.gitignore
vendored
Normal file
1
plugins-external/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*/build
|
||||
76
plugins-external/faker/README.md
Normal file
76
plugins-external/faker/README.md
Normal 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).
|
||||
|
||||

|
||||
|
||||
## 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` |
|
||||
24
plugins-external/faker/package.json
Executable file
24
plugins-external/faker/package.json
Executable 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"
|
||||
}
|
||||
}
|
||||
106
plugins-external/faker/src/index.ts
Executable file
106
plugins-external/faker/src/index.ts
Executable 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);
|
||||
},
|
||||
}));
|
||||
}),
|
||||
};
|
||||
234
plugins-external/faker/tests/__snapshots__/init.test.ts.snap
Normal file
234
plugins-external/faker/tests/__snapshots__/init.test.ts.snap
Normal 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",
|
||||
]
|
||||
`;
|
||||
31
plugins-external/faker/tests/init.test.ts
Normal file
31
plugins-external/faker/tests/init.test.ts
Normal 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$/);
|
||||
});
|
||||
});
|
||||
3
plugins-external/faker/tsconfig.json
Executable file
3
plugins-external/faker/tsconfig.json
Executable file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
52
plugins-external/httpsnippet/README.md
Normal file
52
plugins-external/httpsnippet/README.md
Normal 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).
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
24
plugins-external/httpsnippet/package.json
Normal file
24
plugins-external/httpsnippet/package.json
Normal 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"
|
||||
}
|
||||
317
plugins-external/httpsnippet/src/index.ts
Normal file
317
plugins-external/httpsnippet/src/index.ts
Normal 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",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
58
plugins-external/mcp-server/README.md
Normal file
58
plugins-external/mcp-server/README.md
Normal 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.
|
||||
28
plugins-external/mcp-server/package.json
Normal file
28
plugins-external/mcp-server/package.json
Normal 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"
|
||||
}
|
||||
36
plugins-external/mcp-server/src/index.ts
Normal file
36
plugins-external/mcp-server/src/index.ts
Normal 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;
|
||||
}
|
||||
},
|
||||
};
|
||||
72
plugins-external/mcp-server/src/server.ts
Normal file
72
plugins-external/mcp-server/src/server.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
}
|
||||
148
plugins-external/mcp-server/src/tools/folder.ts
Normal file
148
plugins-external/mcp-server/src/tools/folder.ts
Normal 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})` }],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
32
plugins-external/mcp-server/src/tools/helpers.ts
Normal file
32
plugins-external/mcp-server/src/tools/helpers.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
98
plugins-external/mcp-server/src/tools/httpRequest.test.ts
Normal file
98
plugins-external/mcp-server/src/tools/httpRequest.test.ts
Normal 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" });
|
||||
});
|
||||
201
plugins-external/mcp-server/src/tools/httpRequest.ts
Normal file
201
plugins-external/mcp-server/src/tools/httpRequest.ts
Normal 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})` },
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
67
plugins-external/mcp-server/src/tools/schemas.ts
Normal file
67
plugins-external/mcp-server/src/tools/schemas.ts
Normal 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": {}',
|
||||
);
|
||||
59
plugins-external/mcp-server/src/tools/toast.ts
Normal file
59
plugins-external/mcp-server/src/tools/toast.ts
Normal 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}"`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
47
plugins-external/mcp-server/src/tools/window.ts
Normal file
47
plugins-external/mcp-server/src/tools/window.ts
Normal 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",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
24
plugins-external/mcp-server/src/tools/workspace.ts
Normal file
24
plugins-external/mcp-server/src/tools/workspace.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
5
plugins-external/mcp-server/src/types.ts
Normal file
5
plugins-external/mcp-server/src/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { Context } from "@yaakapp/api";
|
||||
|
||||
export interface McpServerContext {
|
||||
yaak: Context;
|
||||
}
|
||||
7
plugins-external/mcp-server/tsconfig.json
Normal file
7
plugins-external/mcp-server/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user