@erickwendel/ciphersuite-mcp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/agents/developer.agent.md +79 -0
- package/.vscode/mcp.json +8 -0
- package/README.md +150 -0
- package/package.json +32 -0
- package/refs.txt +1 -0
- package/src/index.ts +13 -0
- package/src/mcp.ts +168 -0
- package/tests/helpers.ts +19 -0
- package/tests/mcp.test.ts +109 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Node.js + TypeScript coding agent. Implements features, fixes bugs, and refactors with test-driven discipline. Uses SOLID principles, dependency injection, and immutable patterns. LLM prompts in files, all external calls mocked in tests.
|
|
3
|
+
tools: ['vscode', 'execute', 'read', 'edit', 'search', 'web', 'agent', 'context7/*', 'todo']
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Mission
|
|
7
|
+
|
|
8
|
+
Make minimal, safe edits that are proven by tests.
|
|
9
|
+
|
|
10
|
+
## Success Criteria
|
|
11
|
+
|
|
12
|
+
A task is done when:
|
|
13
|
+
0. Typescript types don't show errors / no warnings
|
|
14
|
+
1. Relevant test file(s) pass
|
|
15
|
+
2. Full test suite passes
|
|
16
|
+
3. User acceptance criteria met
|
|
17
|
+
|
|
18
|
+
## Scope
|
|
19
|
+
|
|
20
|
+
**Will do:**
|
|
21
|
+
- Implement features with tests
|
|
22
|
+
- Fix bugs with regression tests
|
|
23
|
+
- Refactor while preserving behavior
|
|
24
|
+
- Integrate LLM features (prompts in files, mocked in tests)
|
|
25
|
+
|
|
26
|
+
**Won't do:**
|
|
27
|
+
- Introduce unsafe patterns (`eval`, shell injection, secrets in logs)
|
|
28
|
+
- Proceed with ambiguous requirements (will ask questions first)
|
|
29
|
+
- Add dependencies without justification
|
|
30
|
+
- Reorganize files unless requested
|
|
31
|
+
- Move Typescript types to separate files (keep types co-located, never create a types.ts)
|
|
32
|
+
- Don't create index.ts files to re-export modules
|
|
33
|
+
|
|
34
|
+
## Required User Inputs
|
|
35
|
+
|
|
36
|
+
Ask if missing:
|
|
37
|
+
- Acceptance criteria and expected behavior
|
|
38
|
+
- Current vs expected behavior (for bugs)
|
|
39
|
+
- Constraints (Node version, environment)
|
|
40
|
+
|
|
41
|
+
## Core Principles
|
|
42
|
+
|
|
43
|
+
### Code Design
|
|
44
|
+
- **Immutability**: Pure functions, no mutations, side effects at edges
|
|
45
|
+
- **Single Responsibility**: One clear purpose per module/function/file
|
|
46
|
+
- **Dependency Injection**: Pass dependencies via constructors/parameters
|
|
47
|
+
- **Type Safety**: Explicit types, avoid `any`, co-locate with code
|
|
48
|
+
|
|
49
|
+
### Configuration
|
|
50
|
+
- Store all env vars and static values in config files
|
|
51
|
+
- No hardcoded values in business logic
|
|
52
|
+
|
|
53
|
+
### LLM Integration
|
|
54
|
+
- Prompts in files (`prompts/*.txt`), never inline
|
|
55
|
+
- All LLM calls through injected interface (e.g., `LLMClient`)
|
|
56
|
+
- Mock LLM responses deterministically in tests
|
|
57
|
+
|
|
58
|
+
### Testing (Node.js test runner)
|
|
59
|
+
- Use `node:test` with `node:assert/strict`
|
|
60
|
+
- Use fixures files for case scenarios
|
|
61
|
+
- Test the full pipeline end-to-end
|
|
62
|
+
- Mock only external boundaries (HTTP, LLM, DB)
|
|
63
|
+
- Run targeted tests first, then full suite
|
|
64
|
+
|
|
65
|
+
### Security
|
|
66
|
+
- Treat all input as untrusted
|
|
67
|
+
- Validate and sanitize appropriately
|
|
68
|
+
- Never log or expose secrets
|
|
69
|
+
|
|
70
|
+
## Workflow
|
|
71
|
+
|
|
72
|
+
For each task:
|
|
73
|
+
1. **Plan**: Brief summary of what changes and why
|
|
74
|
+
2. **Edit**: Minimal modifications to existing files
|
|
75
|
+
3. **Test**: Add/update tests, run targeted suite
|
|
76
|
+
4. **Verify**: Run full test suite
|
|
77
|
+
5. **Summary**: Note tradeoffs or follow-ups
|
|
78
|
+
|
|
79
|
+
Ask clarifying questions when behavior, security, or architecture is unclear.
|
package/.vscode/mcp.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# @erickwendel/ciphersuite-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that provides AES-256-CBC encryption and decryption tools, a resource describing the algorithm, and ready-to-use prompts — all runnable directly inside VS Code Copilot Chat.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
| Capability | Name | Description |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| 🔧 Tool | `encrypt_message` | Encrypts any plain-text message with a passphrase |
|
|
12
|
+
| 🔧 Tool | `decrypt_message` | Decrypts a previously encrypted message with the same passphrase |
|
|
13
|
+
| 📄 Resource | `encryption://info` | Returns details about the algorithm, key derivation, and output format |
|
|
14
|
+
| 💬 Prompt | `encrypt_message_prompt` | Pre-built prompt that asks the agent to encrypt a message |
|
|
15
|
+
| 💬 Prompt | `decrypt_message_prompt` | Pre-built prompt that asks the agent to decrypt a message |
|
|
16
|
+
|
|
17
|
+
### How encryption works
|
|
18
|
+
|
|
19
|
+
- **Algorithm**: AES-256-CBC
|
|
20
|
+
- **Key derivation**: `scrypt(passphrase, fixedSalt, 32)` — you pass any passphrase string; the server derives a strong 32-byte key automatically
|
|
21
|
+
- **Output format**: `<IV in hex>:<ciphertext in hex>` — keep the full string to decrypt later
|
|
22
|
+
- **IV**: a fresh random 16-byte IV is generated on every encryption call, so the same message encrypted twice produces different output
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Prerequisites
|
|
27
|
+
|
|
28
|
+
- **Node.js v24+** (see `engines` in `package.json`)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
No build step is needed — the server runs TypeScript directly via Node.js native TypeScript support.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Using in VS Code
|
|
43
|
+
|
|
44
|
+
### 1. Add the MCP server configuration
|
|
45
|
+
|
|
46
|
+
Create (or open) `.vscode/mcp.json` in your workspace and add:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"ciphersuite-mcp": {
|
|
52
|
+
"command": "node",
|
|
53
|
+
"args": ["--experimental-strip-types", "ABSOLUTE_PATH_TO_PROJECT/src/index.ts"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
or via npm
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"ciphersuite-mcp": {
|
|
64
|
+
"command": "npx",
|
|
65
|
+
"args": ["-y", "@erickwendel/ciphersuite-mcp"]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> **Tip:** You can also add this server to your user-level MCP config at `~/.vscode/mcp.json` to make it available in every workspace.
|
|
72
|
+
|
|
73
|
+
### 2. Reload VS Code
|
|
74
|
+
|
|
75
|
+
Open the Command Palette (`Cmd+Shift+P`) and run **Developer: Reload Window** (or just restart VS Code).
|
|
76
|
+
|
|
77
|
+
### 3. Use it in Copilot Chat
|
|
78
|
+
|
|
79
|
+
Open Copilot Chat (Agent mode) and try:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
Encrypt the message "Hello, World!" using the passphrase "my-secret-key"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
Decrypt this message: a3f1...:<ciphertext> using the passphrase "my-secret-key"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
Show me the encryption://info resource
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The agent will automatically call the appropriate tool and return the result.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Running the MCP Inspector
|
|
98
|
+
|
|
99
|
+
The MCP Inspector lets you explore and test all tools, resources, and prompts interactively in a browser UI:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npm run mcp:inspect
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This opens the inspector at `http://localhost:5173` and connects it to the running server.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Running tests
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Run all tests once
|
|
113
|
+
npm test
|
|
114
|
+
|
|
115
|
+
# Run tests in watch mode (with debugger)
|
|
116
|
+
npm run test:dev
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The test suite covers:
|
|
120
|
+
|
|
121
|
+
- Encrypting a message
|
|
122
|
+
- Decrypting a message with the correct passphrase
|
|
123
|
+
- Listing and reading the `encryption://info` resource
|
|
124
|
+
- Fetching both prompts
|
|
125
|
+
- Error: decrypting with the wrong passphrase
|
|
126
|
+
- Error: decrypting a malformed ciphertext
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Project structure
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
src/
|
|
134
|
+
index.ts # Entry point — connects the server to stdio transport
|
|
135
|
+
mcp.ts # All tools, resources, and prompts are registered here
|
|
136
|
+
tests/
|
|
137
|
+
mcp.test.ts
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Available scripts
|
|
143
|
+
|
|
144
|
+
| Script | Description |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `npm start` | Start the server (used by MCP clients) |
|
|
147
|
+
| `npm run dev` | Start with file-watch and Node.js inspector |
|
|
148
|
+
| `npm test` | Run all tests |
|
|
149
|
+
| `npm run test:dev` | Run tests in watch mode |
|
|
150
|
+
| `npm run mcp:inspect` | Open the MCP Inspector UI |
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@erickwendel/ciphersuite-mcp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"commit": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/index.ts",
|
|
11
|
+
"dev": "node --watch --inspect src/index.ts",
|
|
12
|
+
"test": "node --test tests/**/*.test.ts",
|
|
13
|
+
"mcp:inspect": "npx @modelcontextprotocol/inspector node src/index.ts",
|
|
14
|
+
"test:dev": "node --inspect --test --watch tests/**/*.test.ts",
|
|
15
|
+
"test:unit:dev": "node --inspect --test --watch tests/**/*unit.test.ts",
|
|
16
|
+
"test:unit": "node --inspect --test tests/**/*unit.test.ts",
|
|
17
|
+
"test:e2e:dev": "node --inspect --test --watch tests/**/*e2e.test.ts",
|
|
18
|
+
"test:e2e": "node --inspect --test tests/**/*e2e.test.ts"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [],
|
|
21
|
+
"author": "erickwendel",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": "v24.14.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
29
|
+
"@types/node": "^24.11.0",
|
|
30
|
+
"zod": "^3.25.76"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/refs.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
https://modelcontextprotocol.io/docs/tools/inspector
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { server } from "./mcp.ts";
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
const transport = new StdioServerTransport();
|
|
6
|
+
await server.connect(transport);
|
|
7
|
+
console.error("Encrypt MCP Server running on stdio");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
main().catch((error) => {
|
|
11
|
+
console.error("Fatal error in main():", error);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'node:crypto';
|
|
3
|
+
import z from "zod";
|
|
4
|
+
|
|
5
|
+
export const server = new McpServer({
|
|
6
|
+
name: "@erickwendel/ciphersuite-mcp",
|
|
7
|
+
version: "0.0.1",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
// Fixed salt — same passphrase always derives the same key so decryption works across calls
|
|
12
|
+
const SALT = 'mcp-encrypter-salt';
|
|
13
|
+
|
|
14
|
+
function deriveKey(passphrase: string): Buffer {
|
|
15
|
+
return scryptSync(passphrase, SALT, 32);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function encrypt(text: string, key: string): string {
|
|
19
|
+
const iv = randomBytes(16);
|
|
20
|
+
const cipher = createCipheriv('aes-256-cbc', deriveKey(key), iv);
|
|
21
|
+
const encrypted = Buffer.concat([
|
|
22
|
+
cipher.update(Buffer.from(text, 'utf8')),
|
|
23
|
+
cipher.final(),
|
|
24
|
+
]);
|
|
25
|
+
return `${iv.toString('hex')}:${encrypted.toString('hex')}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function decrypt(encryptedText: string, key: string): string {
|
|
29
|
+
const [ivHex, ...rest] = encryptedText.split(':');
|
|
30
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
31
|
+
const encrypted = Buffer.from(rest.join(':'), 'hex');
|
|
32
|
+
const decipher = createDecipheriv('aes-256-cbc', deriveKey(key), iv);
|
|
33
|
+
const decrypted = Buffer.concat([
|
|
34
|
+
decipher.update(encrypted),
|
|
35
|
+
decipher.final(),
|
|
36
|
+
]);
|
|
37
|
+
return decrypted.toString('utf8');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
server.registerTool(
|
|
42
|
+
"encrypt_message",
|
|
43
|
+
{
|
|
44
|
+
description: "Encrypt a message",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
message: z.string().describe("The message to encrypt"),
|
|
47
|
+
encryptionKey: z.string().describe("Any passphrase to use for encryption — the server derives a strong key from it automatically"),
|
|
48
|
+
},
|
|
49
|
+
outputSchema: {
|
|
50
|
+
encryptedMessage: z.string().describe("The encrypted message (format: iv:ciphertext)"),
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
async ({ message, encryptionKey }) => {
|
|
54
|
+
try {
|
|
55
|
+
const encryptedMessage = encrypt(message, encryptionKey);
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: encryptedMessage }],
|
|
58
|
+
structuredContent: { encryptedMessage },
|
|
59
|
+
};
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return {
|
|
62
|
+
isError: true,
|
|
63
|
+
content: [{ type: "text", text: `Failed to encrypt message! Check if the message and encryption key are correct. Error details: ${err instanceof Error ? err.message : String(err)}` }],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
server.registerTool(
|
|
70
|
+
"decrypt_message",
|
|
71
|
+
{
|
|
72
|
+
description: "Decrypt a message that was encrypted with the encrypt_message tool",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
encryptedMessage: z.string().describe("The encrypted message to decrypt (format: iv:ciphertext)"),
|
|
75
|
+
encryptionKey: z.string().describe("The same passphrase used during encryption"),
|
|
76
|
+
},
|
|
77
|
+
outputSchema: {
|
|
78
|
+
decryptedMessage: z.string().describe("The decrypted plain-text message"),
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
async ({ encryptedMessage, encryptionKey }) => {
|
|
82
|
+
try {
|
|
83
|
+
const decryptedMessage = decrypt(encryptedMessage, encryptionKey);
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: decryptedMessage }],
|
|
86
|
+
structuredContent: { decryptedMessage },
|
|
87
|
+
};
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return {
|
|
90
|
+
isError: true,
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: "text",
|
|
94
|
+
text: `Failed to decrypt message! Check if the encrypted message is correct and if the encryption key matches the one used for encryption. Error details: ${err instanceof Error ? err.message : String(err)}`,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
server.registerResource(
|
|
103
|
+
"encryption://info",
|
|
104
|
+
"encryption://info",
|
|
105
|
+
{ description: "Describes the encryption algorithm, key requirements, and output format used by this server." },
|
|
106
|
+
async () => ({
|
|
107
|
+
contents: [
|
|
108
|
+
{
|
|
109
|
+
uri: "encryption://info",
|
|
110
|
+
mimeType: "text/plain",
|
|
111
|
+
text: `
|
|
112
|
+
Algorithm : AES-256-CBC
|
|
113
|
+
Key derivation: scrypt (passphrase + fixed server salt → 32-byte key)
|
|
114
|
+
Output format: <16-byte IV in hex>:<ciphertext in hex> (separated by ":")
|
|
115
|
+
Notes:
|
|
116
|
+
- Users pass any passphrase — the server derives a strong 32-byte key automatically using scrypt.
|
|
117
|
+
- A random IV is generated for every encryption — the same message encrypted twice will produce different output.
|
|
118
|
+
- Use the exact same passphrase to decrypt.
|
|
119
|
+
- Keep the full "iv:ciphertext" string to decrypt later.
|
|
120
|
+
`.trim(),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
server.registerPrompt(
|
|
127
|
+
"encrypt_message_prompt",
|
|
128
|
+
{
|
|
129
|
+
description: "Prompt to encrypt a plain-text message using the encrypt_message tool",
|
|
130
|
+
argsSchema: {
|
|
131
|
+
message: z.string().describe("The plain-text message to encrypt"),
|
|
132
|
+
encryptionKey: z.string().describe("Any passphrase — the server derives a strong key automatically"),
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
({ message, encryptionKey }) => ({
|
|
136
|
+
messages: [
|
|
137
|
+
{
|
|
138
|
+
role: "user",
|
|
139
|
+
content: {
|
|
140
|
+
type: "text",
|
|
141
|
+
text: `Please encrypt the following message using the encrypt_message tool.\nMessage: ${message}\nEncryption key: ${encryptionKey}`,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
server.registerPrompt(
|
|
149
|
+
"decrypt_message_prompt",
|
|
150
|
+
{
|
|
151
|
+
description: "Prompt to decrypt a message using the decrypt_message tool",
|
|
152
|
+
argsSchema: {
|
|
153
|
+
encryptedMessage: z.string().describe("The iv:ciphertext string to decrypt"),
|
|
154
|
+
encryptionKey: z.string().describe("The same passphrase used during encryption"),
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
({ encryptedMessage, encryptionKey }) => ({
|
|
158
|
+
messages: [
|
|
159
|
+
{
|
|
160
|
+
role: "user",
|
|
161
|
+
content: {
|
|
162
|
+
type: "text",
|
|
163
|
+
text: `Please decrypt the following message using the decrypt_message tool.\nEncrypted message: ${encryptedMessage}\nEncryption key: ${encryptionKey}`,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
}),
|
|
168
|
+
);
|
package/tests/helpers.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
3
|
+
|
|
4
|
+
export async function createTestClient () {
|
|
5
|
+
const transport = new StdioClientTransport({
|
|
6
|
+
command: 'node',
|
|
7
|
+
args: ['--experimental-strip-types', 'src/index.ts']
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const client = new Client({
|
|
11
|
+
name: 'test-client',
|
|
12
|
+
version: '1.0.0'
|
|
13
|
+
}, {
|
|
14
|
+
capabilities: {}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
await client.connect(transport)
|
|
18
|
+
return client
|
|
19
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, after, before } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { createTestClient,} from './helpers.ts'
|
|
4
|
+
import { Client } from '@modelcontextprotocol/sdk/client'
|
|
5
|
+
|
|
6
|
+
async function encryptMessage(client: Client, message: string, key: string) {
|
|
7
|
+
const result = await client.callTool({
|
|
8
|
+
name: "encrypt_message",
|
|
9
|
+
arguments: {
|
|
10
|
+
message,
|
|
11
|
+
encryptionKey: key
|
|
12
|
+
}
|
|
13
|
+
}) as unknown as { structuredContent: { encryptedMessage: string } }
|
|
14
|
+
|
|
15
|
+
return result
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('MCP Tool Tests', async () => {
|
|
19
|
+
let client: Client;
|
|
20
|
+
let key: string;
|
|
21
|
+
|
|
22
|
+
before(async () => {
|
|
23
|
+
client = await createTestClient()
|
|
24
|
+
key = 'my-super-secret-passphrase';
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
after(async () => {
|
|
28
|
+
await client.close()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should encrypt a message', async () => {
|
|
32
|
+
const message = "Hello, World!"
|
|
33
|
+
const result = await encryptMessage(client, message, key)
|
|
34
|
+
|
|
35
|
+
assert.ok(result.structuredContent.encryptedMessage.length > 60, 'Encrypted message should not be empty')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should decrypt a message', async () => {
|
|
39
|
+
|
|
40
|
+
const message = "Hello, World!"
|
|
41
|
+
const encryptedMessage = (await encryptMessage(client, message, key)).structuredContent.encryptedMessage
|
|
42
|
+
|
|
43
|
+
const result = await client.callTool({
|
|
44
|
+
name: "decrypt_message",
|
|
45
|
+
arguments: {
|
|
46
|
+
encryptedMessage: encryptedMessage,
|
|
47
|
+
encryptionKey: key
|
|
48
|
+
}
|
|
49
|
+
}) as unknown as { structuredContent: { decryptedMessage: string } }
|
|
50
|
+
|
|
51
|
+
assert.strictEqual(result.structuredContent.decryptedMessage, message, 'Decrypted message should match original')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should list the encryption://info resource', async () => {
|
|
55
|
+
const { resources } = await client.listResources()
|
|
56
|
+
const info = resources.find(r => r.uri === 'encryption://info')
|
|
57
|
+
assert.ok(info, 'encryption://info resource should be listed')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should read the encryption://info resource', async () => {
|
|
61
|
+
const result = await client.readResource({ uri: 'encryption://info' })
|
|
62
|
+
const content = result.contents[0]
|
|
63
|
+
assert.ok(content, 'Resource should have content')
|
|
64
|
+
assert.ok('text' in content && typeof content.text === 'string' && content.text.includes('AES-256-CBC'), 'Resource should describe the algorithm')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should return the encrypt_message_prompt', async () => {
|
|
68
|
+
const result = await client.getPrompt({
|
|
69
|
+
name: 'encrypt_message_prompt',
|
|
70
|
+
arguments: { message: 'Secret text', encryptionKey: key }
|
|
71
|
+
})
|
|
72
|
+
const text = result.messages[0].content
|
|
73
|
+
assert.ok('text' in text && text.text.includes('encrypt_message'), 'Prompt should reference the encrypt_message tool')
|
|
74
|
+
assert.ok('text' in text && text.text.includes('Secret text'), 'Prompt should include the message')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should return the decrypt_message_prompt', async () => {
|
|
78
|
+
const encryptedMessage = (await encryptMessage(client, 'Prompt test', key)).structuredContent.encryptedMessage
|
|
79
|
+
const result = await client.getPrompt({
|
|
80
|
+
name: 'decrypt_message_prompt',
|
|
81
|
+
arguments: { encryptedMessage, encryptionKey: key }
|
|
82
|
+
})
|
|
83
|
+
const text = result.messages[0].content
|
|
84
|
+
assert.ok('text' in text && text.text.includes('decrypt_message'), 'Prompt should reference the decrypt_message tool')
|
|
85
|
+
assert.ok('text' in text && text.text.includes(encryptedMessage), 'Prompt should include the encrypted message')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should return isError when decrypting with wrong passphrase', async () => {
|
|
89
|
+
const encryptedMessage = (await encryptMessage(client, 'Secret', key)).structuredContent.encryptedMessage
|
|
90
|
+
|
|
91
|
+
const result = await client.callTool({
|
|
92
|
+
name: 'decrypt_message',
|
|
93
|
+
arguments: { encryptedMessage, encryptionKey: 'wrong-passphrase' }
|
|
94
|
+
}) as unknown as { isError: boolean; content: { type: string; text: string }[] }
|
|
95
|
+
|
|
96
|
+
assert.strictEqual(result.isError, true, 'Should return isError: true')
|
|
97
|
+
assert.ok(result.content[0].text.includes('Failed to decrypt'), 'Error message should describe the failure')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should return isError when decrypting a malformed message', async () => {
|
|
101
|
+
const result = await client.callTool({
|
|
102
|
+
name: 'decrypt_message',
|
|
103
|
+
arguments: { encryptedMessage: 'not-valid-ciphertext', encryptionKey: key }
|
|
104
|
+
}) as unknown as { isError: boolean; content: { type: string; text: string }[] }
|
|
105
|
+
|
|
106
|
+
assert.strictEqual(result.isError, true, 'Should return isError: true')
|
|
107
|
+
assert.ok(result.content[0].text.includes('Failed to decrypt'), 'Error message should describe the failure')
|
|
108
|
+
})
|
|
109
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2022"
|
|
7
|
+
],
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"allowSyntheticDefaultImports": true,
|
|
18
|
+
"types": [
|
|
19
|
+
"node"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"exclude": [
|
|
23
|
+
"node_modules",
|
|
24
|
+
"dist"
|
|
25
|
+
]
|
|
26
|
+
}
|