@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.
@@ -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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "servers": {
3
+ "ciphersuite-mcp": {
4
+ "command": "node",
5
+ "args": ["--experimental-strip-types", "/Users/erickwendel/Downloads/projetos/ewit/cursos/ia-devs/pos-grad/modulo03/projetos/04-mcps-do-zero/src/index.ts"]
6
+ }
7
+ }
8
+ }
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
+ );
@@ -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
+ }