Spec Driven Development
MCP Server that facilitates spec-driven development workflows, not just Vibe Coding.
Descripción del creador (original en inglés)
Structural FEM for AI agents: frames, plates, buckling, seismic and Eurocode checks, solved locally.
Estos tres nunca se combinan en una sola puntuación: las opiniones, las mediciones y la popularidad son cosas distintas.
Todavía no escribimos un resumen sencillo para este. Abajo están los datos que leímos del registro.
Elige la app de IA que usas y copia la configuración.
FERS_API_KEYLos valores marcados como secretos son llaves de tu cuenta. Nunca los compartas ni los pegues en un chat.
Necesitas
¿Qué app usas?
Elige tu app arriba. La recordamos, así que la próxima vez el botón dirá “Instalar en …”.
Funciona en Claude para web, escritorio y móvil:
https://ferscloud.com/api/mcpAbre Claude Desktop → Settings → Developer → Edit Config y pega esto en claude_desktop_config.json
{
"mcpServers": {
"fers": {
"command": "npx",
"args": [
"-y",
"@ferscloud/fers-mcp"
],
"env": {
"FERS_API_KEY": "your-value"
}
}
}
}Ejecuta esto en tu terminal
claude mcp add --transport http fers https://ferscloud.com/api/mcpclaude mcp add fers --env FERS_API_KEY=your-value -- npx -y @ferscloud/fers-mcpCompleta los valores vacíos antes de ejecutarlo.
Pégalo en .cursor/mcp.json de tu proyecto, o en ~/.cursor/mcp.json
{
"mcpServers": {
"fers": {
"url": "https://ferscloud.com/api/mcp"
}
}
}Pégalo en .cursor/mcp.json de tu proyecto, o en ~/.cursor/mcp.json
{
"mcpServers": {
"fers": {
"command": "npx",
"args": [
"-y",
"@ferscloud/fers-mcp"
],
"env": {
"FERS_API_KEY": "your-value"
}
}
}
}Tu navegador te pide permiso para abrir la app; luego la app muestra la configuración y te pide confirmar. Los valores de las claves quedan vacíos para que los completes tú.
Pégalo en .vscode/mcp.json de tu proyecto
{
"servers": {
"fers": {
"type": "http",
"url": "https://ferscloud.com/api/mcp"
}
}
}Pégalo en .vscode/mcp.json de tu proyecto
{
"servers": {
"fers": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@ferscloud/fers-mcp"
],
"env": {
"FERS_API_KEY": "your-value"
}
}
}
}Tu navegador te pide permiso para abrir la app; luego la app muestra la configuración y te pide confirmar. Los valores de las claves quedan vacíos para que los completes tú.
ChatGPT solo se conecta a servidores en internet:
https://ferscloud.com/api/mcpLos nombres de los menús pueden variar un poco según el plan y con el tiempo.
Tu elección se guarda en una cookie en este dispositivo; no necesitas cuenta.
Esta configuración se genera a partir de la ficha del creador en el registro. Si el README dice otra cosa, sigue el README.
El servidor pide iniciar sesión antes de mostrar sus herramientas.
Revisamos información que cualquiera puede ver; no hemos leído el código. Las revisiones que no aplican no se cuentan. · Revisado el 7 nov 2026
Quien lo publicó (ferscloud.com) no es el dueño del repo (Jeroen124); puede que otra persona lo haya listado
Todavía no hemos leído este repo en GitHub
Todavía no hemos leído este repo en GitHub
Respondió en 51 ms (HTTP 401)
Declara inicio de sesión con OAuth, así que tu app de IA puede llevarte a iniciar sesión
Aún sin revisar; leemos la lista de herramientas cuando revisamos el endpoint
Comparado con todos los servidores activos de esta categoría en el registro. Las estrellas solo cuentan cuando quien lo publica es dueño del repo.
Superó 2 de 3 revisiones básicas aplicables. No hemos leído el código. Antes de agregar una clave o un token, revisa qué acceso pide el servidor y usa una clave que tenga solo los permisos que necesitas.
Debes completar: FERS_API_KEY
Funciona de las dos formas: instálalo en tu computadora o conéctate a la versión en la nube.
El repo de GitHub cambió por última vez –, y la última versión (2.83.0) llegó al registro anteayer.
Las apps compatibles con MCP, como Claude Desktop, Claude Code, Cursor y VS Code. Se conecta por URL, así que también funcionan las apps que aceptan servidores MCP remotos, como ChatGPT en modo desarrollador.
En la misma categoría: Spec Driven Development, Remote Desktop Commander, FactLot
Servidores de la misma categoría hechos por otros creadores, ordenados por estrellas.
Cómo leer una tarjeta:★ Estrellas de GitHubiTres pilares: reseñas · revisiones · usoiActualizado = fecha en que se publicó la última versión en el registroi
MCP Server that facilitates spec-driven development workflows, not just Vibe Coding.
Remote Desktop Commander: use files, terminal and processes on your own computers from any AI app.
Cited CRE facts and deterministic calculators for agents: rent, CAM, debt sizing, IRR, parcel IDs.
Copiado tal cual del repo del creador. PopNx MCP no lo ha revisado ni lo respalda. Se quitaron HTML, imágenes y scripts. Los enlaces llevan fuera de este sitio.
FERS: structural finite-element analysis (3D frames, trusses, plates, Eurocode 3 checks) for engineers and AI agents. This is its Python SDK and the fers command. The solver is a compiled engine that runs on your machine, with no account and no network call: free for non-commercial use up to 100 members. This SDK is open source (BSD-3-Clause) at github.com/Jeroen124/FERS_core; the engine's licence is under Licence.
It computes linear and second-order (P-Delta) static analysis, natural frequencies, elastic buckling load factors (α_cr), response-spectrum seismic analysis, and EN 1993-1-1 steel member checks, flexural and lateral-torsional buckling included. A model is JSON in and JSON out, written from Python, by the fers command, or by an AI agent through the FERS MCP server, the Claude Code plugin or the agent skill.
pip install FERS
from fers_core import check_beam
model = check_beam(6.0, "IPE200", udl=10_000.0) model.settings.analysis_options.include_member_deflected_shape = True model.run_analysis()
beam = model.resultsbundle.loadcases["Q"].member_results["1"] x_frac, (_, dy, _) = beam.member_displacement_peak ec3 = model.unity_check_results()[0] print(f"M {beam.local_minimums.mz / 1e3:.1f} kNm, sag {dy * 1e3:.1f} mm at x/L {x_frac:.2f}") print(f"EC3 utilization {ec3['max_utilization']:.2f}")
M -45.0 kNm, sag -41.8 mm at x/L 0.50
EC3 utilization 2.85
The sag is 41.3 mm of bending (5wL⁴/384EI) plus 0.4 mm of shear deformation, which the engine includes. The beam fails: the ULS moment of 60.75 kNm exceeds its bending resistance of 51.9 kNm, and with nothing restraining its compression flange, lateral-torsional buckling takes the utilization to 2.85.
fers solve, fers check and fers validate take a model JSON file, print one line (or --json) and say in their exit code whether the numbers can be used; see From the terminal and from coding agents. fers skill --install . gives a coding agent the FERS skill, and MCP clients can use the FERS MCP server. In Claude Code, both come as one plugin: /plugin install fers --marketplace Jeroen124/FERS_core.pip install "FERS[plot]"), section properties meshed from your own dimensions ("FERS[sections]") and DXF import ("FERS[dxf]"); "FERS[all]" installs everything 0.1.x did. Catalogue sections (IPE200, HEB300, UB 457x191x67, W14X30, ...) need no extra.Building a model by hand, create the FERS object first, then its nodes, members and loads. Creating a FERS restarts the id counters, so a node made before it would share an id with one made after it, and the solver refuses node ids that do not run from 1 without gaps.
run_analysis() puts the results on model.resultsbundle. Load cases and load combinations are keyed by name. Within each, nodes and members are keyed by their id as a string, because results round-trip through JSON.
case = model.resultsbundle.loadcases["Load"] # or .loadcombinations["ULS 1"]
case.displacement_nodes["5"].dy # dx dy dz rx ry rz warp case.reaction_nodes["1"].nodal_forces.fy # fx fy fz mx my mz bw beam = case.member_results["4"] beam.local_maximums.mz, beam.local_minimums.mz # envelope along the member beam.local_start_forces.fy # end forces, member axes
start_node_forces, end_node_forces, maximums and minimums are in the global frame; the local_* forces, section_forces and internal_force_series are in the member's own axes, with mz the strong-axis moment. Read a frame's moments from the local ones: outside the XY plane the global mz is not the bending moment, and in a frame in the YZ plane it reads 0.
Since 0.1.98, member_results and displacement_nodes are read-only tables rather than dicts of objects. That is what keeps a large result small in memory. They read like dicts: [], get, in, len, .items(), and iteration in the solver's order. Each lookup builds a fresh object, and nothing in it can be changed:
beam.local_maximums.mz = 0.0 # AttributeError: read-only
editable = case.member_results.copy() # a plain dict of editable objects editable["4"].local_maximums.mz = 0.0 # fine
section_forces, internal_force_series and member_displacements come back as tuples. To test for a table, use isinstance(x, collections.abc.Mapping), not dict. reaction_nodes and plate_results are plain dicts, as before.
from fers_core import FERS
model.save_to_json("beam.json") # the model and its results model = FERS.from_json("beam.json")
FERS.from_json reads any file the solver or the SDK wrote, one entry at a time. A typical result needs less memory than its file to load, and at most about twice the file's size; before 0.1.98 it needed 15 to 20 times. For a large model, let the solver write the file itself. The result then never passes through Python as one string:
model.run_analysis_to_file("beam_results.json")
solved = FERS.from_json("beam_results.json")
fers_core/examples/809_Reading_Results.py runs all of this end to end.
Saving a loaded model writes back everything the solver reads, with the same ids. That includes what the file defines but nothing uses yet, such as a section an editor keeps in its library. Nodes, members and the rest created after loading get ids the loaded model does not use, so saving never lets a new object replace a loaded one.
AnalysisOptions(result_filter=[...]) keeps or drops whole result blocks. result_requests goes further. Each ResultRequest names a block and narrows it to chosen members or nodes, load combinations, components and one member end:
from fers_core import ResultBlock, ResultRequest
from fers_core.builders import check_beam from fers_core.result_requests import nodes_of from fers_core.unity_checks import classification
model = check_beam(5.0, "IPE300", udl=10000.0) # one "Beam", one ULS combination beams = classification("Beam") model.settings.analysis_options.result_requests = [ ResultRequest(ResultBlock.LOCAL_ENVELOPES, members=[beams], limit_state="ULS", components=["fx", "my", "mz"]), ResultRequest(ResultBlock.NODE_DISPLACEMENTS, nodes=nodes_of(beams), limit_state="ULS", components=["dy"]), ResultRequest(ResultBlock.REACTIONS, limit_state="ULS"), ] model.run_analysis()
The values load into the same member_results, displacement_nodes and reaction_nodes, so code that reads them keeps working. Anything that was not requested reads as `None`, not zero. A check that reads a value nobody asked for therefore fails instead of passing on a zero.
members takes the unity checks' selectors: classification(...), members([...]), member_sets([...]) and all_members(). Several selectors form a union.nodes takes nodes([...]), or nodes_of(*selectors) for the end nodes of the selected members.limit_state, as unity checks match them, or by load_combination_ids. To select by limit state, set limit_state on each LoadCombination; the solver does not read situation.limit_state nor load_combination_ids also covers the load cases.end="start" or end="end" keeps one end of the end-force and end-displacement blocks.section_forces, internal_force_series, a component the block does not have, a limit state no combination carries, and result_filter set as well.The values themselves are on model.resultsbundle.selections, one entry per request and group of result sets. Each entry's values is a read-only numpy array shaped (result_sets, ids, fields, components), with each axis listed beside it:
sel = model.resultsbundle.selections[0]
sel["group"] # "loadcombinations" or "loadcases" sel["result_sets"], sel["ids"] # combination names, member or node ids sel["fields"], sel["components"] # e.g. ["local_maximums", "local_minimums"], ["fx", "my", "mz"] my = sel"values".index("my")] # every set, member and field
save_to_json writes the selections back as the solver wrote them, so a saved result reloads unchanged. fers_core/examples/810_Result_Requests.py runs this end to end.
A rail running over beams and resting on each one is continuous along its length, but free to turn on every beam. A MemberHinge releases one member end, so releasing both rail ends at a beam splits the rail into two spans. Put the rail's members in a member set of their own and give the set a ScissorHinge instead. No link member or extra node is needed:
from fers_core import FERS, DistributedLoad, Material, Member, MemberSet, NodalSupport, Node, ScissorHinge, Section
from fers_core.supports.supportcondition import SupportCondition as C
model = FERS() steel = Material(name="S355", e_mod=210e9, g_mod=81e9, density=7850.0, yield_stress=355e6) rail_section = Section(name="rail", material=steel, i_y=1e-6, i_z=1e-6, j=1e-7, area=1e-3) beam_section = Section(name="beam", material=steel, i_y=1e-4, i_z=1e-4, j=1e-4, area=5e-3)
turns = {"X": C.fixed(), "Y": C.free(), "Z": C.free()} a, m, b = Node(0.0, 0.0, 0.0), Node(1.0, 0.0, 0.0), Node(2.0, 0.0, 0.0) a.nodal_support = NodalSupport(rotation_conditions=turns) b.nodal_support = NodalSupport( displacement_conditions={"X": C.free(), "Y": C.fixed(), "Z": C.fixed()}, rotation_conditions=turns ) beam_start, beam_end = Node(1.0, 0.0, -0.5), Node(1.0, 0.0, 0.5) beam_start.nodal_support = beam_end.nodal_support = NodalSupport() # clamped
rail_1 = Member(start_node=a, end_node=m, section=rail_section) rail_2 = Member(start_node=m, end_node=b, section=rail_section) model.add_member_set(MemberSet( members=[rail_1, rail_2], scissor_hinge=ScissorHinge(rotational_release_y=0.0, rotational_release_z=0.0), )) model.add_member_set(MemberSet(members=[ Member(start_node=beam_start, end_node=m, section=beam_section), Member(start_node=m, end_node=beam_end, section=beam_section), ]))
span = model.create_load_case(name="span") DistributedLoad(member=rail_1, load_case=span, magnitude=1000.0, end_magnitude=1000.0, direction=(0, -1, 0)) model.run_analysis() mz = model.resultsbundle.loadcases["span"].member_results[str(rail_1.id)].local_end_forces.mz print(f"{abs(mz):.1f} Nm") # 62.4; a knife edge gives qL²/16 = 62.5, a rigid joint 122.6
MemberSet(scissor_hinge_nodes=[...]) lists where the hinge applies, for a beam that turns on some of the members it meets and is joined to the rest.MemberHinge release doeSolo se muestra el principio; lee el resto en el repo