@panagos/jojoscript 0.9.0 → 0.9.3

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # JojoScript 0.9.0
1
+ # JojoScript 0.9.3
2
2
 
3
3
  JojoScript is a small JavaScript-compatible language and runtime for
4
4
  expressive, composable pipelines. It adds lightweight syntax for declarations,
@@ -6,8 +6,8 @@ functions, pattern matching, type annotations, and pipelines while preserving
6
6
  ordinary JavaScript. Its compiler uses a lightweight lexer and source
7
7
  transformations rather than a full AST, keeping the toolchain compact. The
8
8
  project also provides lazy and async collection operators, checkpointing,
9
- retries, metrics, pipeline graphing, CLI workflows, effect handlers, and
10
- VS Code editor support.
9
+ retries, metrics, pipeline graphing, CLI workflows, effect handlers, MCP
10
+ support, and VS Code editor support.
11
11
 
12
12
  See [CHANGELOG.md](CHANGELOG.md) for release history and
13
13
  [example/](example/) for a complete runnable application that exercises
@@ -237,6 +237,26 @@ becomes:
237
237
  divide(100, value)
238
238
  ```
239
239
 
240
+ #### Property access stages
241
+
242
+ A pipeline stage may also start with a dot to read a field or invoke a method
243
+ on the piped value directly:
244
+
245
+ ```jojo
246
+ users |> .length
247
+ name |> .trim() |> .toUpperCase()
248
+ ```
249
+
250
+ becomes:
251
+
252
+ ```js
253
+ users.length
254
+ name.trim().toUpperCase()
255
+ ```
256
+
257
+ This is handy for compact pipelines where the previous value is the receiver of
258
+ an ordinary JavaScript property or method call.
259
+
240
260
  #### `catch` / `orElse` control stages
241
261
 
242
262
  `catch` and `orElse` provide inline error handling without leaving pipeline
@@ -719,6 +739,29 @@ never through `|>`) and also uses pipeline syntax with overlapping names,
719
739
  the compiler merges the auto-imported names into the existing import
720
740
  instead of emitting a second, colliding import declaration.
721
741
 
742
+ ### MCP support
743
+
744
+ JojoScript includes a small MCP-style JSON-RPC runtime for Node.js. It does
745
+ not add MCP-specific language syntax; tool implementations remain ordinary
746
+ JavaScript functions.
747
+
748
+ ```js
749
+ import { createMcpServer, runMcpStdioServer } from "jojoscript/runtime"
750
+
751
+ const server = createMcpServer({
752
+ tools: {
753
+ echo: args => ({ content: [{ type: "text", text: args.value }] })
754
+ }
755
+ })
756
+
757
+ await runMcpStdioServer(server)
758
+ ```
759
+
760
+ The server accepts `tools/call` requests and returns JSON-RPC responses on
761
+ standard output. It is intentionally minimal: supporting tool registration,
762
+ request dispatch, structured errors, and the stdin/stdout transport without any
763
+ external NPM packages.
764
+
722
765
  ## Example
723
766
 
724
767
  See [example/](example/). It is a complete runnable project covering every
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panagos/jojoscript",
3
- "version": "0.9.0",
3
+ "version": "0.9.3",
4
4
  "description": "A tiny JavaScript-compatible language layer with Jojo syntax and pipelines.",
5
5
  "license": "Unlicense",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -1,33 +1,33 @@
1
- export {
2
- compile,
3
- compileFile,
4
- compileDirectory
5
- } from './compiler.js'
6
-
7
- export {
8
- transformPipelines,
9
- emitPipeline,
10
- findPipelineStdlibFunctions
11
- } from './pipeline-parser.js'
12
-
13
- export {
14
- transformMatchExpressions
15
- } from './match-parser.js'
16
-
17
- export {
18
- transformTypeAnnotations
19
- } from './type-annotations.js'
20
-
21
- export {
22
- analyzePipelines,
23
- buildGraph,
24
- renderGraphText,
25
- renderGraphDot
26
- } from './pipeline-ir.js'
27
-
28
- export {
29
- transformEffectDeclarations,
30
- transformHandleBlocks,
31
- findEffectDeclarations,
32
- findHandleBlocks
33
- } from './effect-parser.js'
1
+ export {
2
+ compile,
3
+ compileFile,
4
+ compileDirectory
5
+ } from './compiler.js'
6
+
7
+ export {
8
+ transformPipelines,
9
+ emitPipeline,
10
+ findPipelineStdlibFunctions
11
+ } from './pipeline-parser.js'
12
+
13
+ export {
14
+ transformMatchExpressions
15
+ } from './match-parser.js'
16
+
17
+ export {
18
+ transformTypeAnnotations
19
+ } from './type-annotations.js'
20
+
21
+ export {
22
+ analyzePipelines,
23
+ buildGraph,
24
+ renderGraphText,
25
+ renderGraphDot
26
+ } from './pipeline-ir.js'
27
+
28
+ export {
29
+ transformEffectDeclarations,
30
+ transformHandleBlocks,
31
+ findEffectDeclarations,
32
+ findHandleBlocks
33
+ } from './effect-parser.js'
@@ -112,9 +112,18 @@ function splitArguments(tokens) {
112
112
  return result
113
113
  }
114
114
 
115
+ function isPropertyAccessName(name) {
116
+ return /^\.[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(name)
117
+ }
118
+
115
119
  function parseStage(source, tokens) {
116
120
  if (!tokens.length) throw new SyntaxError('Empty pipeline stage')
117
121
 
122
+ const bareName = text(source, tokens).trim()
123
+ if (isPropertyAccessName(bareName)) {
124
+ return { name: bareName, args: [], propertyCall: false }
125
+ }
126
+
118
127
  if (tokens.every((token, index) =>
119
128
  index % 2 === 0
120
129
  ? token.type === 'identifier'
@@ -145,6 +154,14 @@ function parseStage(source, tokens) {
145
154
 
146
155
  const name = text(source, tokens.slice(0, openIndex)).trim()
147
156
 
157
+ if (isPropertyAccessName(name)) {
158
+ const args = splitArguments(
159
+ tokens.slice(openIndex + 1, closeIndex)
160
+ ).map(argument => text(source, argument).trim())
161
+
162
+ return { name, args, propertyCall: true }
163
+ }
164
+
148
165
  if (!/^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(name)) {
149
166
  throw new SyntaxError(`Invalid pipeline function: ${name}`)
150
167
  }
@@ -310,6 +327,33 @@ export function emitPipeline(pipeline, options = {}) {
310
327
  continue
311
328
  }
312
329
 
330
+ if (stage.name === 'pass') {
331
+ if (stage.args.length === 0) continue
332
+ if (stage.args.length === 1 && stage.args[0] === '_') {
333
+ continue
334
+ }
335
+ throw new SyntaxError('the `pass` pipeline stage takes no arguments or a single `_` placeholder')
336
+ }
337
+
338
+ if (stage.name.startsWith('.')) {
339
+ const args = stage.args.slice()
340
+ const placeholderIndex = args.indexOf('_')
341
+ if (placeholderIndex !== -1) {
342
+ args[placeholderIndex] = expression
343
+ }
344
+
345
+ if (stage.propertyCall || args.length > 0) {
346
+ expression = `${expression}${stage.name}(${args.join(', ')})`
347
+ } else {
348
+ expression = `${expression}${stage.name}`
349
+ }
350
+
351
+ if (options.profile) {
352
+ expression = `traceNode(${expression}, ${metaLiteral(pipelineId, stage.nodeId, stage.name)})`
353
+ }
354
+ continue
355
+ }
356
+
313
357
  if (stage.name === 'retry' && stage.standaloneRetry) {
314
358
  const meta = metaLiteral(pipelineId, stage.nodeId, 'retry')
315
359
  expression = `(await retry(async () => (${expression}), ${stage.args[0]}, ${meta})())`
@@ -67,4 +67,9 @@ export {
67
67
  MemoryEffectLog,
68
68
  FileEffectLog
69
69
  } from './effect-log.js'
70
+ export {
71
+ McpServer,
72
+ createMcpServer,
73
+ runMcpStdioServer
74
+ } from './mcp.js'
70
75
 
@@ -0,0 +1,93 @@
1
+ /*
2
+ * Minimal MCP-style JSON-RPC transport for Node.js. The server is protocol-
3
+ * focused rather than introducing MCP-specific JojoScript syntax; tool
4
+ * implementations remain ordinary JavaScript functions.
5
+ */
6
+
7
+ import { createInterface } from 'node:readline'
8
+
9
+ export class McpServer {
10
+ constructor({ tools = {} } = {}) {
11
+ this.tools = new Map(Object.entries(tools))
12
+ }
13
+
14
+ async handleRequest(message) {
15
+ if (!message || typeof message !== 'object') {
16
+ return {
17
+ jsonrpc: '2.0',
18
+ id: null,
19
+ error: {
20
+ code: -32600,
21
+ message: 'Invalid Request'
22
+ }
23
+ }
24
+ }
25
+
26
+ if (message.method !== 'tools/call') {
27
+ return {
28
+ jsonrpc: '2.0',
29
+ id: message.id ?? null,
30
+ error: {
31
+ code: -32601,
32
+ message: 'Method not found'
33
+ }
34
+ }
35
+ }
36
+
37
+ const toolName = message.params?.name
38
+ const toolArgs = message.params?.arguments ?? {}
39
+ const tool = this.tools.get(toolName)
40
+
41
+ if (!tool || typeof tool !== 'function') {
42
+ return {
43
+ jsonrpc: '2.0',
44
+ id: message.id ?? null,
45
+ error: {
46
+ code: -32602,
47
+ message: `Unknown tool: ${toolName}`
48
+ }
49
+ }
50
+ }
51
+
52
+ try {
53
+ const result = await tool(toolArgs)
54
+ return {
55
+ jsonrpc: '2.0',
56
+ id: message.id ?? null,
57
+ result
58
+ }
59
+ } catch (error) {
60
+ return {
61
+ jsonrpc: '2.0',
62
+ id: message.id ?? null,
63
+ error: {
64
+ code: -32603,
65
+ message: error instanceof Error ? error.message : String(error)
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ export function createMcpServer(options = {}) {
73
+ return new McpServer(options)
74
+ }
75
+
76
+ export async function runMcpStdioServer(server, options = {}) {
77
+ const input = options.input ?? process.stdin
78
+ const output = options.output ?? process.stdout
79
+
80
+ const lines = Array.isArray(input)
81
+ ? input.values()
82
+ : createInterface({ input, crlfDelay: Infinity })
83
+
84
+ for await (const line of lines) {
85
+ if (!line.trim()) continue
86
+
87
+ const message = JSON.parse(line)
88
+ const response = await server.handleRequest(message)
89
+ output.write(`${JSON.stringify(response)}\n`)
90
+ }
91
+
92
+ return 0
93
+ }